diff --git a/lib/experimental/content-types/load.php b/lib/experimental/content-types/load.php index 06ad6e60f3a870..d15d9fb8d3540a 100644 --- a/lib/experimental/content-types/load.php +++ b/lib/experimental/content-types/load.php @@ -1,7 +1,6 @@ + + navigate( { to: `/${ tabId }` } ) + } + > +
+ + + { __( 'Post Types' ) } + + + { __( 'Taxonomies' ) } + + +
+
+ { children } + + ); +} diff --git a/packages/content-types/src/components/layout/style.scss b/packages/content-types/src/components/layout/style.scss new file mode 100644 index 00000000000000..e1a67e783c3f8c --- /dev/null +++ b/packages/content-types/src/components/layout/style.scss @@ -0,0 +1,23 @@ +@use "@wordpress/base-styles/colors" as *; +@use "@wordpress/base-styles/variables" as *; +@use "@wordpress/base-styles/mixins" as *; + +// Size the stage to the viewport so DataViews owns its own scroll and +// the sticky footer stays visible. Note: when the wp-admin sidebar +// (#adminmenu) is taller than the viewport, #wpwrap still grows to +// fit it, so empty space may appear below DataViews. +.boot-layout__stage:has(.content-types-page) { + height: calc(100vh - #{$admin-bar-height-big}); + + @include break-medium { + height: calc(100vh - #{$admin-bar-height}); + } +} + +.content-types-page { + padding: var(--wpds-dimension-padding-lg); +} + +.content-types-tabs-wrapper { + border-bottom: 1px solid $gray-100; +} diff --git a/packages/content-types/src/constants.ts b/packages/content-types/src/constants.ts new file mode 100644 index 00000000000000..36db0bf86392da --- /dev/null +++ b/packages/content-types/src/constants.ts @@ -0,0 +1,10 @@ +// URL path segments for the top-level tabs / list pages. +export const POST_TYPES_PATH = '/post-types'; +export const TAXONOMIES_PATH = '/taxonomies'; + +// Core-data postType names that store the user-defined records. +export const POST_TYPE_ENTITY = 'wp_user_post_type'; +export const TAXONOMY_ENTITY = 'wp_user_taxonomy'; + +// Sentinel for the edit-route param when creating a new record. +export const NEW_ID = 'new'; diff --git a/packages/content-types/src/index.ts b/packages/content-types/src/index.ts new file mode 100644 index 00000000000000..4eee948665227a --- /dev/null +++ b/packages/content-types/src/index.ts @@ -0,0 +1,13 @@ +export { Layout } from './components/layout'; +export { PostTypesList } from './post-types/list'; +export { PostTypeEdit } from './post-types/edit'; +export { TaxonomiesList } from './taxonomies/list'; +export { TaxonomyEdit } from './taxonomies/edit'; + +export { + POST_TYPES_PATH, + TAXONOMIES_PATH, + POST_TYPE_ENTITY, + TAXONOMY_ENTITY, + NEW_ID, +} from './constants'; diff --git a/packages/user-post-types/src/lock-unlock.ts b/packages/content-types/src/lock-unlock.ts similarity index 91% rename from packages/user-post-types/src/lock-unlock.ts rename to packages/content-types/src/lock-unlock.ts index 6dd7a66434650e..861ca1c7d55fd6 100644 --- a/packages/user-post-types/src/lock-unlock.ts +++ b/packages/content-types/src/lock-unlock.ts @@ -6,5 +6,5 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri export const { lock, unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', - '@wordpress/user-post-types' + '@wordpress/content-types' ); diff --git a/packages/user-post-types/src/actions/activate.ts b/packages/content-types/src/post-types/actions/activate.ts similarity index 100% rename from packages/user-post-types/src/actions/activate.ts rename to packages/content-types/src/post-types/actions/activate.ts diff --git a/packages/user-post-types/src/actions/deactivate.ts b/packages/content-types/src/post-types/actions/deactivate.ts similarity index 100% rename from packages/user-post-types/src/actions/deactivate.ts rename to packages/content-types/src/post-types/actions/deactivate.ts diff --git a/packages/user-post-types/src/actions/delete.tsx b/packages/content-types/src/post-types/actions/delete.tsx similarity index 98% rename from packages/user-post-types/src/actions/delete.tsx rename to packages/content-types/src/post-types/actions/delete.tsx index 73483202ab8fe8..d8f48b50bb3741 100644 --- a/packages/user-post-types/src/actions/delete.tsx +++ b/packages/content-types/src/post-types/actions/delete.tsx @@ -15,6 +15,7 @@ import { Stack, Text } from '@wordpress/ui'; * Internal dependencies */ import type { CoreDataError, PostTypeFormData } from '../types'; +import { POST_TYPE_ENTITY } from '../../constants'; function DeletePostTypeModal( { items, @@ -40,7 +41,7 @@ function DeletePostTypeModal( { itemsToDelete.map( ( item ) => deleteEntityRecord( 'postType', - 'wp_user_post_type', + POST_TYPE_ENTITY, item.id as number, { force: true }, { throwOnError: true } diff --git a/packages/user-post-types/src/actions/duplicate.tsx b/packages/content-types/src/post-types/actions/duplicate.tsx similarity index 98% rename from packages/user-post-types/src/actions/duplicate.tsx rename to packages/content-types/src/post-types/actions/duplicate.tsx index 83cf9365fa65b0..6c54ca6fab00e6 100644 --- a/packages/user-post-types/src/actions/duplicate.tsx +++ b/packages/content-types/src/post-types/actions/duplicate.tsx @@ -27,6 +27,7 @@ import { } from '../fields/general'; import { serializeForSave } from '../utils'; import type { CoreDataError, PostTypeFormData } from '../types'; +import { POST_TYPE_ENTITY } from '../../constants'; const SLUG_MAX_LENGTH = 20; @@ -93,7 +94,7 @@ function DuplicatePostTypeModal( { try { await saveEntityRecord( 'postType', - 'wp_user_post_type', + POST_TYPE_ENTITY, serializeForSave( data ), { throwOnError: true } ); diff --git a/routes/post-types/actions/edit.tsx b/packages/content-types/src/post-types/actions/edit.ts similarity index 76% rename from routes/post-types/actions/edit.tsx rename to packages/content-types/src/post-types/actions/edit.ts index 6757c833ba179d..a8f00dc6311248 100644 --- a/routes/post-types/actions/edit.tsx +++ b/packages/content-types/src/post-types/actions/edit.ts @@ -5,7 +5,12 @@ import type { Action } from '@wordpress/dataviews'; import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useNavigate } from '@wordpress/route'; -import type { PostTypeFormData } from '@wordpress/user-post-types'; + +/** + * Internal dependencies + */ +import type { PostTypeFormData } from '../types'; +import { POST_TYPES_PATH } from '../../constants'; export function useEditPostTypeAction(): Action< PostTypeFormData > { const navigate = useNavigate(); @@ -19,7 +24,7 @@ export function useEditPostTypeAction(): Action< PostTypeFormData > { return; } navigate( { - to: `/edit/${ item.id }`, + to: `${ POST_TYPES_PATH }/${ item.id }`, } ); }, } ), diff --git a/routes/post-types/actions/quick-edit.tsx b/packages/content-types/src/post-types/actions/quick-edit.tsx similarity index 94% rename from routes/post-types/actions/quick-edit.tsx rename to packages/content-types/src/post-types/actions/quick-edit.tsx index f7012341673a4f..6caa100fc36776 100644 --- a/routes/post-types/actions/quick-edit.tsx +++ b/packages/content-types/src/post-types/actions/quick-edit.tsx @@ -15,20 +15,24 @@ import { useMemo, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { Stack, Text } from '@wordpress/ui'; + +/** + * Internal dependencies + */ import { defaultForm, hasArchiveField, hierarchicalField, pluralLabelField, publicField, - serializeForSave, singularLabelField, statusField, useSlugField, useTaxonomiesField, - type PostTypeFormData, -} from '@wordpress/user-post-types'; -import '../style.scss'; +} from '../fields'; +import type { PostTypeFormData } from '../types'; +import { serializeForSave } from '../utils'; +import { POST_TYPE_ENTITY } from '../../constants'; function QuickEditPostTypeModal( { items, @@ -70,7 +74,7 @@ function QuickEditPostTypeModal( { try { await saveEntityRecord( 'postType', - 'wp_user_post_type', + POST_TYPE_ENTITY, serializeForSave( { ...data, id: item.id } ), { throwOnError: true } ); diff --git a/packages/user-post-types/src/actions/utils.ts b/packages/content-types/src/post-types/actions/utils.ts similarity index 97% rename from packages/user-post-types/src/actions/utils.ts rename to packages/content-types/src/post-types/actions/utils.ts index 8d813b5a882acb..e93efc4165445c 100644 --- a/packages/user-post-types/src/actions/utils.ts +++ b/packages/content-types/src/post-types/actions/utils.ts @@ -9,6 +9,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import type { CoreDataError, PostTypeFormData } from '../types'; +import { POST_TYPE_ENTITY } from '../../constants'; type Status = PostTypeFormData[ 'status' ]; @@ -48,7 +49,7 @@ export function createStatusAction( itemsToUpdate.map( ( item ) => saveEntityRecord( 'postType', - 'wp_user_post_type', + POST_TYPE_ENTITY, { id: item.id, status: config.targetStatus }, { throwOnError: true } ) diff --git a/packages/content-types/src/post-types/edit.tsx b/packages/content-types/src/post-types/edit.tsx new file mode 100644 index 00000000000000..020cb1c9e95d2a --- /dev/null +++ b/packages/content-types/src/post-types/edit.tsx @@ -0,0 +1,324 @@ +/** + * WordPress dependencies + */ +import { Breadcrumbs, Page } from '@wordpress/admin-ui'; +import { Button } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + DataForm, + useFormValidity, + type Field, + type Form, +} from '@wordpress/dataviews'; +import { useMemo, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useNavigate, useParams } from '@wordpress/route'; +import { Stack } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import { + addNewField, + addNewItemLabelField, + allItemsField, + archivesField, + attributesField, + descriptionField, + editItemField, + featuredImageField, + filterItemsListField, + generalForm, + hasArchiveField, + hierarchicalField, + insertIntoItemField, + itemsListField, + itemsListNavigationField, + labelsForm, + menuNameField, + newItemField, + notFoundField, + notFoundInTrashField, + parentItemColonField, + pluralLabelField, + publicField, + removeFeaturedImageField, + searchItemsField, + setFeaturedImageField, + showInRestField, + singularLabelField, + statusField, + supportsField, + uploadedToThisItemField, + useFeaturedImageField, + useSlugField, + useTaxonomiesField, + viewItemField, + viewItemsField, +} from './fields'; +import type { PostTypeFormData, PostTypeRecord } from './types'; +import { BLANK_RECORD, serializeForSave, toFormData } from './utils'; +import { NEW_ID, POST_TYPE_ENTITY, POST_TYPES_PATH } from '../constants'; + +type PostTypePageProps = { + isAddMode: boolean; + initialData: PostTypeFormData; + title: string; + breadcrumbLabel: string; + subTitle: string; + onSaved?: ( saved: PostTypeFormData & { id: number } ) => void; +}; + +export function PostTypeEdit() { + const { id } = useParams( { from: `${ POST_TYPES_PATH }/$id` } ); + const navigate = useNavigate(); + const isAddMode = id === NEW_ID; + const postTypeId = parseInt( id, 10 ); + const record = useSelect( + ( select ) => { + return ( + ! isAddMode && + // beforeLoad (route.ts) guarantees the record is in cache. + select( coreStore ).getEntityRecord< PostTypeRecord >( + 'postType', + POST_TYPE_ENTITY, + postTypeId + )! + ); + }, + [ isAddMode, postTypeId ] + ); + const initialData = + ! isAddMode && record ? toFormData( record ) : BLANK_RECORD; + const title = isAddMode ? __( 'Add post type' ) : initialData.title.raw; + const commonProps = { initialData, title }; + const postTypePageProps: PostTypePageProps = isAddMode + ? { + ...commonProps, + isAddMode: true, + breadcrumbLabel: __( 'Add new' ), + subTitle: __( + 'Define a new post type. Fill in the essentials under General; expand Labels to customize.' + ), + onSaved: ( saved ) => + navigate( { + to: `${ POST_TYPES_PATH }/${ saved.id }`, + } ), + } + : { + ...commonProps, + isAddMode: false, + breadcrumbLabel: title, + subTitle: __( + 'Edit this post type. Expand the Labels section to adjust labels.' + ), + }; + + // key remounts PostTypePage when navigating between records so in-flight + // form state doesn't leak across different post types. + return ; +} + +function PostTypePage( { + isAddMode, + initialData, + title, + breadcrumbLabel, + subTitle, + onSaved, +}: PostTypePageProps ) { + const [ data, setData ] = useState< PostTypeFormData >( initialData ); + const [ isSaving, setIsSaving ] = useState( false ); + const originalSlug = ! isAddMode ? initialData.slug : undefined; + const slugField = useSlugField( originalSlug, data.slug ); + const taxonomiesField = useTaxonomiesField(); + const fields = useMemo< Field< PostTypeFormData >[] >( + () => [ + // General + pluralLabelField, + singularLabelField, + slugField, + descriptionField, + taxonomiesField, + supportsField, + publicField, + hierarchicalField, + hasArchiveField, + showInRestField, + statusField, + // Labels + menuNameField, + allItemsField, + addNewField, + addNewItemLabelField, + editItemField, + newItemField, + viewItemField, + viewItemsField, + searchItemsField, + notFoundField, + notFoundInTrashField, + parentItemColonField, + archivesField, + attributesField, + insertIntoItemField, + uploadedToThisItemField, + featuredImageField, + setFeaturedImageField, + removeFeaturedImageField, + useFeaturedImageField, + filterItemsListField, + itemsListNavigationField, + itemsListField, + ], + [ slugField, taxonomiesField ] + ); + + const form = useMemo< Form >( + () => ( { + layout: { type: 'card', isCollapsible: true }, + fields: [ + { + id: 'general', + label: __( 'General' ), + description: __( + 'Core identity, taxonomies, supports, and activation.' + ), + layout: { + type: 'card', + isCollapsible: true, + isOpened: true, + }, + children: generalForm.fields, + }, + { + id: 'labels', + label: __( 'Labels' ), + description: __( + 'Override the text WordPress shows in admin lists, menus, and forms. Leave blank to use defaults derived from the plural and singular names.' + ), + layout: { + type: 'card', + isCollapsible: true, + isOpened: false, + }, + children: labelsForm.fields, + }, + ], + } ), + [] + ); + + const { validity, isValid } = useFormValidity( data, fields, form ); + + const formId = useInstanceId( PostTypePage, 'post-type-form' ); + + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function onSave() { + if ( isSaving || ! isValid ) { + return; + } + setIsSaving( true ); + try { + const saved = ( await saveEntityRecord( + 'postType', + POST_TYPE_ENTITY, + serializeForSave( data ), + { throwOnError: true } + ) ) as { id: number } | undefined; + const successMessage = isAddMode + ? sprintf( + /* translators: %s: post type plural label. */ + __( '"%s" post type created.' ), + data.title.raw + ) + : sprintf( + /* translators: %s: post type plural label. */ + __( '"%s" post type updated.' ), + data.title.raw + ); + createSuccessNotice( successMessage, { type: 'snackbar' } ); + if ( saved?.id !== undefined ) { + onSaved?.( { ...data, id: saved.id } ); + } + } catch ( error: any ) { + let errorMessage: string; + if ( error?.message && error?.code !== 'unknown_error' ) { + errorMessage = error.message; + } else if ( isAddMode ) { + errorMessage = __( 'Failed to create post type.' ); + } else { + errorMessage = __( 'Failed to update post type.' ); + } + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } finally { + setIsSaving( false ); + } + } + + return ( + + } + subTitle={ subTitle } + actions={ + + } + > + { + event.preventDefault(); + onSave(); + } } + /> + } + > + + data={ data } + fields={ fields } + form={ form } + validity={ validity } + onChange={ ( edits ) => + setData( + ( prev ) => + ( { + ...prev, + ...edits, + } ) as PostTypeFormData + ) + } + /> + + + ); +} diff --git a/packages/user-post-types/src/fields/general.tsx b/packages/content-types/src/post-types/fields/general.tsx similarity index 98% rename from packages/user-post-types/src/fields/general.tsx rename to packages/content-types/src/post-types/fields/general.tsx index 8b170a15919ec1..e9fcca384e36ad 100644 --- a/packages/user-post-types/src/fields/general.tsx +++ b/packages/content-types/src/post-types/fields/general.tsx @@ -19,7 +19,8 @@ import { cleanForSlug } from '@wordpress/url'; /** * Internal dependencies */ -import { unlock } from '../lock-unlock'; +import { unlock } from '../../lock-unlock'; +import { POST_TYPE_ENTITY } from '../../constants'; import { SUPPORT_FEATURES, usePublicTaxonomies } from '../utils'; import type { PostTypeFormData, SupportFeature } from '../types'; @@ -283,7 +284,7 @@ export function useSlugField( // types are checked through `registeredPostTypes` above. const drafts = await resolveSelect( coreStore - ).getEntityRecords( 'postType', 'wp_user_post_type', { + ).getEntityRecords( 'postType', POST_TYPE_ENTITY, { slug, status: 'draft', _fields: 'id,name', diff --git a/packages/user-post-types/src/fields/index.ts b/packages/content-types/src/post-types/fields/index.ts similarity index 100% rename from packages/user-post-types/src/fields/index.ts rename to packages/content-types/src/post-types/fields/index.ts diff --git a/packages/user-post-types/src/fields/labels.tsx b/packages/content-types/src/post-types/fields/labels.tsx similarity index 100% rename from packages/user-post-types/src/fields/labels.tsx rename to packages/content-types/src/post-types/fields/labels.tsx diff --git a/routes/post-types/stage.tsx b/packages/content-types/src/post-types/list.tsx similarity index 66% rename from routes/post-types/stage.tsx rename to packages/content-types/src/post-types/list.tsx index 8104456b293a36..41561a9ad3105e 100644 --- a/routes/post-types/stage.tsx +++ b/packages/content-types/src/post-types/list.tsx @@ -1,13 +1,20 @@ /** * WordPress dependencies */ -import { Page } from '@wordpress/admin-ui'; import { Button } from '@wordpress/components'; import { DataViews, type View } from '@wordpress/dataviews'; import { useEntityRecords } from '@wordpress/core-data'; import { useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useNavigate } from '@wordpress/route'; + +/** + * Internal dependencies + */ +import activateAction from './actions/activate'; +import deactivateAction from './actions/deactivate'; +import deletePostTypeAction from './actions/delete'; +import duplicatePostTypeAction from './actions/duplicate'; import { hasArchiveField, hierarchicalField, @@ -15,21 +22,14 @@ import { statusField, supportsField, titleField, - toFormData, useSlugField, useTaxonomiesField, - activateAction, - deactivateAction, - deletePostTypeAction, - duplicatePostTypeAction, - type PostTypeRecord, -} from '@wordpress/user-post-types'; - -/** - * Internal dependencies - */ -import { quickEditPostTypeAction, useEditPostTypeAction } from './actions'; -import './style.scss'; +} from './fields'; +import type { PostTypeFormData, PostTypeRecord } from './types'; +import { toFormData } from './utils'; +import { useEditPostTypeAction } from './actions/edit'; +import quickEditPostTypeAction from './actions/quick-edit'; +import { NEW_ID, POST_TYPE_ENTITY, POST_TYPES_PATH } from '../constants'; const defaultLayouts = { table: {}, @@ -44,7 +44,7 @@ const DEFAULT_VIEW: View = { layout: {}, }; -function PostTypesPage() { +export function PostTypesList() { const navigate = useNavigate(); const [ view, setView ] = useState< View >( DEFAULT_VIEW ); const editAction = useEditPostTypeAction(); @@ -91,7 +91,7 @@ function PostTypesPage() { const { records, isResolving, hasResolved, totalItems, totalPages } = useEntityRecords< PostTypeRecord >( 'postType', - 'wp_user_post_type', + POST_TYPE_ENTITY, queryArgs ); const data = useMemo( @@ -106,38 +106,36 @@ function PostTypesPage() { [ totalItems, totalPages ] ); return ( - + data={ data } + fields={ fields } + actions={ postTypeActions } + view={ view } + onChangeView={ setView } + isLoading={ isResolving || ! hasResolved } + paginationInfo={ paginationInfo } + defaultLayouts={ defaultLayouts } + getItemId={ ( item ) => String( item.id ) } + isItemClickable={ () => true } + onClickItem={ ( item ) => + navigate( { + to: `${ POST_TYPES_PATH }/${ item.id }`, + } ) + } + header={ } - > - String( item.id ) } - isItemClickable={ () => true } - onClickItem={ ( item ) => - navigate( { to: `/edit/${ item.id }` } ) - } - /> - + /> ); } - -export const stage = PostTypesPage; diff --git a/packages/user-post-types/src/types.ts b/packages/content-types/src/post-types/types.ts similarity index 100% rename from packages/user-post-types/src/types.ts rename to packages/content-types/src/post-types/types.ts diff --git a/packages/user-post-types/src/utils.ts b/packages/content-types/src/post-types/utils.ts similarity index 100% rename from packages/user-post-types/src/utils.ts rename to packages/content-types/src/post-types/utils.ts diff --git a/routes/taxonomies/style.scss b/packages/content-types/src/style.scss similarity index 69% rename from routes/taxonomies/style.scss rename to packages/content-types/src/style.scss index 41679282cdf43e..f892ecd632ebf7 100644 --- a/routes/taxonomies/style.scss +++ b/packages/content-types/src/style.scss @@ -1,19 +1,17 @@ @use "@wordpress/base-styles/variables" as *; @use "@wordpress/base-styles/colors" as *; -@use "@wordpress/base-styles/mixins" as *; +@use "./components/layout/style.scss" as *; -// Size the stage to the viewport so DataViews owns its own scroll and -// the sticky footer stays visible. Note: when the wp-admin sidebar -// (#adminmenu) is taller than the viewport, #wpwrap still grows to -// fit it, so empty space may appear below DataViews. -.boot-layout__stage:has(.taxonomies-page) { - height: calc(100vh - #{$admin-bar-height-big}); - - @include break-medium { - height: calc(100vh - #{$admin-bar-height}); - } +.post-type-form, +.taxonomy-form { + box-sizing: border-box; + width: 100%; + max-width: 680px; + margin: 0 auto; + padding: 24px; } +.dataviews-action-modal__quick-edit-post-type, .dataviews-action-modal__quick-edit-taxonomy { justify-content: flex-end; align-items: stretch; @@ -28,7 +26,7 @@ animation-name: none; @media (prefers-reduced-motion: no-preference) { - animation-name: quick-edit-taxonomy-slide-in-right; + animation-name: content-types-quick-edit-slide-in-right; animation-duration: 0.2s; animation-timing-function: ease-out; } @@ -38,7 +36,7 @@ animation-name: none; @media (prefers-reduced-motion: no-preference) { - animation-name: quick-edit-taxonomy-slide-out-right; + animation-name: content-types-quick-edit-slide-out-right; animation-duration: 0.2s; animation-timing-function: ease-out; } @@ -49,6 +47,7 @@ overflow-y: auto; } + .dataviews-action-modal__quick-edit-post-type-header, .dataviews-action-modal__quick-edit-taxonomy-header { position: sticky; top: 0; @@ -57,10 +56,12 @@ padding: $grid-unit-20 $grid-unit-30; } + .dataviews-action-modal__quick-edit-post-type-content, .dataviews-action-modal__quick-edit-taxonomy-content { padding: 0 $grid-unit-30; } + .dataviews-action-modal__quick-edit-post-type-footer, .dataviews-action-modal__quick-edit-taxonomy-footer { position: sticky; bottom: 0; @@ -75,7 +76,7 @@ } } -@keyframes quick-edit-taxonomy-slide-in-right { +@keyframes content-types-quick-edit-slide-in-right { from { transform: translateX(100%); } @@ -84,7 +85,7 @@ } } -@keyframes quick-edit-taxonomy-slide-out-right { +@keyframes content-types-quick-edit-slide-out-right { from { transform: translateX(0); } diff --git a/packages/user-taxonomies/src/actions/activate.ts b/packages/content-types/src/taxonomies/actions/activate.ts similarity index 100% rename from packages/user-taxonomies/src/actions/activate.ts rename to packages/content-types/src/taxonomies/actions/activate.ts diff --git a/packages/user-taxonomies/src/actions/deactivate.ts b/packages/content-types/src/taxonomies/actions/deactivate.ts similarity index 100% rename from packages/user-taxonomies/src/actions/deactivate.ts rename to packages/content-types/src/taxonomies/actions/deactivate.ts diff --git a/packages/user-taxonomies/src/actions/delete.tsx b/packages/content-types/src/taxonomies/actions/delete.tsx similarity index 98% rename from packages/user-taxonomies/src/actions/delete.tsx rename to packages/content-types/src/taxonomies/actions/delete.tsx index 6bb6211f911d07..5c470e2b038538 100644 --- a/packages/user-taxonomies/src/actions/delete.tsx +++ b/packages/content-types/src/taxonomies/actions/delete.tsx @@ -15,6 +15,7 @@ import { Stack, Text } from '@wordpress/ui'; * Internal dependencies */ import type { CoreDataError, TaxonomyFormData } from '../types'; +import { TAXONOMY_ENTITY } from '../../constants'; function DeleteTaxonomyModal( { items, @@ -40,7 +41,7 @@ function DeleteTaxonomyModal( { itemsToDelete.map( ( item ) => deleteEntityRecord( 'postType', - 'wp_user_taxonomy', + TAXONOMY_ENTITY, item.id as number, { force: true }, { throwOnError: true } diff --git a/packages/user-taxonomies/src/actions/duplicate.tsx b/packages/content-types/src/taxonomies/actions/duplicate.tsx similarity index 98% rename from packages/user-taxonomies/src/actions/duplicate.tsx rename to packages/content-types/src/taxonomies/actions/duplicate.tsx index 48672bd30a8906..12465c68b49567 100644 --- a/packages/user-taxonomies/src/actions/duplicate.tsx +++ b/packages/content-types/src/taxonomies/actions/duplicate.tsx @@ -27,6 +27,7 @@ import { } from '../fields/general'; import { serializeForSave } from '../utils'; import type { CoreDataError, TaxonomyFormData } from '../types'; +import { TAXONOMY_ENTITY } from '../../constants'; const SLUG_MAX_LENGTH = 32; @@ -93,7 +94,7 @@ function DuplicateTaxonomyModal( { try { await saveEntityRecord( 'postType', - 'wp_user_taxonomy', + TAXONOMY_ENTITY, serializeForSave( data ), { throwOnError: true } ); diff --git a/routes/taxonomies/actions/edit.tsx b/packages/content-types/src/taxonomies/actions/edit.ts similarity index 76% rename from routes/taxonomies/actions/edit.tsx rename to packages/content-types/src/taxonomies/actions/edit.ts index efb8984c2be74f..3d6ddb2da9688a 100644 --- a/routes/taxonomies/actions/edit.tsx +++ b/packages/content-types/src/taxonomies/actions/edit.ts @@ -5,7 +5,12 @@ import type { Action } from '@wordpress/dataviews'; import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useNavigate } from '@wordpress/route'; -import type { TaxonomyFormData } from '@wordpress/user-taxonomies'; + +/** + * Internal dependencies + */ +import type { TaxonomyFormData } from '../types'; +import { TAXONOMIES_PATH } from '../../constants'; export function useEditTaxonomyAction(): Action< TaxonomyFormData > { const navigate = useNavigate(); @@ -19,7 +24,7 @@ export function useEditTaxonomyAction(): Action< TaxonomyFormData > { return; } navigate( { - to: `/edit/${ item.id }`, + to: `${ TAXONOMIES_PATH }/${ item.id }`, } ); }, } ), diff --git a/routes/taxonomies/actions/quick-edit.tsx b/packages/content-types/src/taxonomies/actions/quick-edit.tsx similarity index 94% rename from routes/taxonomies/actions/quick-edit.tsx rename to packages/content-types/src/taxonomies/actions/quick-edit.tsx index c1b7f8585e53ed..9cd50aafee19db 100644 --- a/routes/taxonomies/actions/quick-edit.tsx +++ b/packages/content-types/src/taxonomies/actions/quick-edit.tsx @@ -15,19 +15,23 @@ import { useMemo, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { Stack, Text } from '@wordpress/ui'; + +/** + * Internal dependencies + */ import { defaultForm, hierarchicalField, pluralLabelField, publicField, - serializeForSave, singularLabelField, statusField, useObjectTypeField, useSlugField, - type TaxonomyFormData, -} from '@wordpress/user-taxonomies'; -import '../style.scss'; +} from '../fields'; +import type { TaxonomyFormData } from '../types'; +import { serializeForSave } from '../utils'; +import { TAXONOMY_ENTITY } from '../../constants'; function QuickEditTaxonomyModal( { items, @@ -68,7 +72,7 @@ function QuickEditTaxonomyModal( { try { await saveEntityRecord( 'postType', - 'wp_user_taxonomy', + TAXONOMY_ENTITY, serializeForSave( { ...data, id: item.id } ), { throwOnError: true } ); diff --git a/packages/user-taxonomies/src/actions/utils.ts b/packages/content-types/src/taxonomies/actions/utils.ts similarity index 97% rename from packages/user-taxonomies/src/actions/utils.ts rename to packages/content-types/src/taxonomies/actions/utils.ts index 59e23378b7a392..2bcc532c32cf07 100644 --- a/packages/user-taxonomies/src/actions/utils.ts +++ b/packages/content-types/src/taxonomies/actions/utils.ts @@ -9,6 +9,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import type { CoreDataError, TaxonomyFormData } from '../types'; +import { TAXONOMY_ENTITY } from '../../constants'; type Status = TaxonomyFormData[ 'status' ]; @@ -48,7 +49,7 @@ export function createStatusAction( itemsToUpdate.map( ( item ) => saveEntityRecord( 'postType', - 'wp_user_taxonomy', + TAXONOMY_ENTITY, { id: item.id, status: config.targetStatus }, { throwOnError: true } ) diff --git a/packages/content-types/src/taxonomies/edit.tsx b/packages/content-types/src/taxonomies/edit.tsx new file mode 100644 index 00000000000000..8d9697249f6d87 --- /dev/null +++ b/packages/content-types/src/taxonomies/edit.tsx @@ -0,0 +1,334 @@ +/** + * WordPress dependencies + */ +import { Breadcrumbs, Page } from '@wordpress/admin-ui'; +import { Button } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + DataForm, + useFormValidity, + type Field, + type Form, +} from '@wordpress/dataviews'; +import { useMemo, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useNavigate, useParams } from '@wordpress/route'; +import { Stack } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import { + addNewItemLabelField, + addOrRemoveItemsField, + allItemsField, + backToItemsField, + chooseFromMostUsedField, + descriptionField, + editItemField, + generalFormFields, + hierarchicalField, + labelsActionsField, + labelsFormFields, + menuNameField, + newItemNameField, + notFoundField, + parentItemColonField, + parentItemField, + pluralLabelField, + popularItemsField, + publicField, + publiclyQueryableField, + searchItemsField, + separateItemsField, + showAdminColumnField, + showInMenuField, + showInNavMenusField, + showInQuickEditField, + showInRestField, + showTagcloudField, + showUiField, + singularLabelField, + statusField, + updateItemField, + useObjectTypeField, + useSlugField, + viewItemField, + visibilityFormFields, +} from './fields'; +import type { TaxonomyFormData, TaxonomyRecord } from './types'; +import { BLANK_RECORD, serializeForSave, toFormData } from './utils'; +import { NEW_ID, TAXONOMIES_PATH, TAXONOMY_ENTITY } from '../constants'; + +type TaxonomyPageProps = { + isAddMode: boolean; + initialData: TaxonomyFormData; + title: string; + breadcrumbLabel: string; + subTitle: string; + onSaved?: ( saved: TaxonomyFormData & { id: number } ) => void; +}; + +export function TaxonomyEdit() { + const { id } = useParams( { from: `${ TAXONOMIES_PATH }/$id` } ); + const navigate = useNavigate(); + const isAddMode = id === NEW_ID; + const taxonomyId = parseInt( id, 10 ); + const record = useSelect( + ( select ) => { + return ( + ! isAddMode && + // beforeLoad (route.ts) guarantees the record is in cache. + select( coreStore ).getEntityRecord< TaxonomyRecord >( + 'postType', + TAXONOMY_ENTITY, + taxonomyId + )! + ); + }, + [ isAddMode, taxonomyId ] + ); + const initialData = + ! isAddMode && record ? toFormData( record ) : BLANK_RECORD; + const title = isAddMode ? __( 'Add taxonomy' ) : initialData.title.raw; + const commonProps = { initialData, title }; + const taxonomyPageProps: TaxonomyPageProps = isAddMode + ? { + ...commonProps, + isAddMode: true, + breadcrumbLabel: __( 'Add new' ), + subTitle: __( + 'Define a new taxonomy. Fill in the essentials under General; expand Labels to customize.' + ), + onSaved: ( saved ) => + navigate( { + to: `${ TAXONOMIES_PATH }/${ saved.id }`, + } ), + } + : { + ...commonProps, + isAddMode: false, + breadcrumbLabel: title, + subTitle: __( + 'Edit this taxonomy. Expand the Labels section to adjust labels.' + ), + }; + + // key remounts TaxonomyPage when navigating between records so in-flight + // form state doesn't leak across different taxonomies. + return ; +} + +function TaxonomyPage( { + isAddMode, + initialData, + title, + breadcrumbLabel, + subTitle, + onSaved, +}: TaxonomyPageProps ) { + const [ data, setData ] = useState< TaxonomyFormData >( initialData ); + const [ isSaving, setIsSaving ] = useState( false ); + const originalSlug = ! isAddMode ? initialData.slug : undefined; + const slugField = useSlugField( originalSlug, data.slug ); + const objectTypeField = useObjectTypeField(); + const fields = useMemo< Field< TaxonomyFormData >[] >( + () => [ + // General + pluralLabelField, + singularLabelField, + slugField, + descriptionField, + objectTypeField, + hierarchicalField, + statusField, + // Visibility + publicField, + showInRestField, + publiclyQueryableField, + showUiField, + showInMenuField, + showInQuickEditField, + showAdminColumnField, + showInNavMenusField, + showTagcloudField, + // Labels + labelsActionsField, + menuNameField, + allItemsField, + editItemField, + viewItemField, + updateItemField, + addNewItemLabelField, + newItemNameField, + searchItemsField, + notFoundField, + backToItemsField, + parentItemField, + popularItemsField, + separateItemsField, + parentItemColonField, + addOrRemoveItemsField, + chooseFromMostUsedField, + ], + [ slugField, objectTypeField ] + ); + + const form = useMemo< Form >( + () => ( { + layout: { type: 'card', isCollapsible: true }, + fields: [ + { + id: 'general', + label: __( 'General' ), + description: __( + 'Core identity, post types, and activation.' + ), + layout: { + type: 'card', + isCollapsible: true, + isOpened: true, + }, + children: generalFormFields, + }, + { + id: 'visibility', + label: __( 'Visibility' ), + description: __( + 'Where this taxonomy appears: REST API, admin UI, and front-end surfaces.' + ), + layout: { + type: 'card', + isCollapsible: true, + isOpened: false, + }, + children: visibilityFormFields, + }, + { + id: 'labels', + label: __( 'Labels' ), + layout: { + type: 'card', + isCollapsible: true, + isOpened: false, + }, + children: labelsFormFields, + }, + ], + } ), + [] + ); + + const { validity, isValid } = useFormValidity( data, fields, form ); + + const formId = useInstanceId( TaxonomyPage, 'taxonomy-form' ); + + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function onSave() { + if ( isSaving || ! isValid ) { + return; + } + setIsSaving( true ); + try { + const saved = ( await saveEntityRecord( + 'postType', + TAXONOMY_ENTITY, + serializeForSave( data ), + { throwOnError: true } + ) ) as { id: number } | undefined; + const successMessage = isAddMode + ? sprintf( + /* translators: %s: taxonomy plural label. */ + __( '"%s" taxonomy created.' ), + data.title.raw + ) + : sprintf( + /* translators: %s: taxonomy plural label. */ + __( '"%s" taxonomy updated.' ), + data.title.raw + ); + createSuccessNotice( successMessage, { type: 'snackbar' } ); + if ( saved?.id !== undefined ) { + onSaved?.( { ...data, id: saved.id } ); + } + } catch ( error: any ) { + let errorMessage: string; + if ( error?.message && error?.code !== 'unknown_error' ) { + errorMessage = error.message; + } else if ( isAddMode ) { + errorMessage = __( 'Failed to create taxonomy.' ); + } else { + errorMessage = __( 'Failed to update taxonomy.' ); + } + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } finally { + setIsSaving( false ); + } + } + + return ( + + } + subTitle={ subTitle } + actions={ + + } + > + { + event.preventDefault(); + onSave(); + } } + /> + } + > + + data={ data } + fields={ fields } + form={ form } + validity={ validity } + onChange={ ( edits ) => + setData( + ( prev ) => + ( { + ...prev, + ...edits, + } ) as TaxonomyFormData + ) + } + /> + + + ); +} diff --git a/packages/user-taxonomies/src/fields/general.tsx b/packages/content-types/src/taxonomies/fields/general.tsx similarity index 98% rename from packages/user-taxonomies/src/fields/general.tsx rename to packages/content-types/src/taxonomies/fields/general.tsx index 302fa8500fdfc0..02ebb743f8dc1b 100644 --- a/packages/user-taxonomies/src/fields/general.tsx +++ b/packages/content-types/src/taxonomies/fields/general.tsx @@ -19,7 +19,8 @@ import { cleanForSlug } from '@wordpress/url'; /** * Internal dependencies */ -import { unlock } from '../lock-unlock'; +import { unlock } from '../../lock-unlock'; +import { TAXONOMY_ENTITY } from '../../constants'; import { usePublicPostTypes } from '../utils'; import type { TaxonomyFormData } from '../types'; import { booleanField } from './utils'; @@ -181,7 +182,7 @@ export function useSlugField( // We only need to query for `drafts` because published taxonomies are checked through `registeredTaxonomies` above. const drafts = await resolveSelect( coreStore - ).getEntityRecords( 'postType', 'wp_user_taxonomy', { + ).getEntityRecords( 'postType', TAXONOMY_ENTITY, { slug, status: 'draft', _fields: 'id,name', diff --git a/packages/user-taxonomies/src/fields/index.ts b/packages/content-types/src/taxonomies/fields/index.ts similarity index 100% rename from packages/user-taxonomies/src/fields/index.ts rename to packages/content-types/src/taxonomies/fields/index.ts diff --git a/packages/user-taxonomies/src/fields/labels.tsx b/packages/content-types/src/taxonomies/fields/labels.tsx similarity index 100% rename from packages/user-taxonomies/src/fields/labels.tsx rename to packages/content-types/src/taxonomies/fields/labels.tsx diff --git a/packages/user-taxonomies/src/fields/utils.ts b/packages/content-types/src/taxonomies/fields/utils.ts similarity index 100% rename from packages/user-taxonomies/src/fields/utils.ts rename to packages/content-types/src/taxonomies/fields/utils.ts diff --git a/packages/user-taxonomies/src/fields/visibility.tsx b/packages/content-types/src/taxonomies/fields/visibility.tsx similarity index 100% rename from packages/user-taxonomies/src/fields/visibility.tsx rename to packages/content-types/src/taxonomies/fields/visibility.tsx diff --git a/routes/taxonomies/stage.tsx b/packages/content-types/src/taxonomies/list.tsx similarity index 66% rename from routes/taxonomies/stage.tsx rename to packages/content-types/src/taxonomies/list.tsx index 420d6ec3ea2ddf..b0d4d0a793da79 100644 --- a/routes/taxonomies/stage.tsx +++ b/packages/content-types/src/taxonomies/list.tsx @@ -1,33 +1,33 @@ /** * WordPress dependencies */ -import { Page } from '@wordpress/admin-ui'; import { Button } from '@wordpress/components'; import { DataViews, type View } from '@wordpress/dataviews'; import { useEntityRecords } from '@wordpress/core-data'; import { useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useNavigate } from '@wordpress/route'; + +/** + * Internal dependencies + */ +import activateAction from './actions/activate'; +import deactivateAction from './actions/deactivate'; +import deleteTaxonomyAction from './actions/delete'; +import duplicateTaxonomyAction from './actions/duplicate'; import { hierarchicalField, publicField, statusField, titleField, - toFormData, useObjectTypeField, useSlugField, - activateAction, - deactivateAction, - deleteTaxonomyAction, - duplicateTaxonomyAction, - type TaxonomyRecord, -} from '@wordpress/user-taxonomies'; - -/** - * Internal dependencies - */ -import { quickEditTaxonomyAction, useEditTaxonomyAction } from './actions'; -import './style.scss'; +} from './fields'; +import type { TaxonomyFormData, TaxonomyRecord } from './types'; +import { toFormData } from './utils'; +import { useEditTaxonomyAction } from './actions/edit'; +import quickEditTaxonomyAction from './actions/quick-edit'; +import { NEW_ID, TAXONOMIES_PATH, TAXONOMY_ENTITY } from '../constants'; const defaultLayouts = { table: {}, @@ -42,7 +42,7 @@ const DEFAULT_VIEW: View = { layout: {}, }; -function TaxonomiesPage() { +export function TaxonomiesList() { const navigate = useNavigate(); const [ view, setView ] = useState< View >( DEFAULT_VIEW ); const editAction = useEditTaxonomyAction(); @@ -91,7 +91,7 @@ function TaxonomiesPage() { const { records, isResolving, hasResolved, totalItems, totalPages } = useEntityRecords< TaxonomyRecord >( 'postType', - 'wp_user_taxonomy', + TAXONOMY_ENTITY, queryArgs ); const data = useMemo( @@ -106,38 +106,36 @@ function TaxonomiesPage() { [ totalItems, totalPages ] ); return ( - + data={ data } + fields={ fields } + actions={ taxonomyActions } + view={ view } + onChangeView={ setView } + isLoading={ isResolving || ! hasResolved } + paginationInfo={ paginationInfo } + defaultLayouts={ defaultLayouts } + getItemId={ ( item ) => String( item.id ) } + isItemClickable={ () => true } + onClickItem={ ( item ) => + navigate( { + to: `${ TAXONOMIES_PATH }/${ item.id }`, + } ) + } + header={ } - > - String( item.id ) } - isItemClickable={ () => true } - onClickItem={ ( item ) => - navigate( { to: `/edit/${ item.id }` } ) - } - /> - + /> ); } - -export const stage = TaxonomiesPage; diff --git a/packages/user-taxonomies/src/types.ts b/packages/content-types/src/taxonomies/types.ts similarity index 100% rename from packages/user-taxonomies/src/types.ts rename to packages/content-types/src/taxonomies/types.ts diff --git a/packages/user-taxonomies/src/utils.ts b/packages/content-types/src/taxonomies/utils.ts similarity index 100% rename from packages/user-taxonomies/src/utils.ts rename to packages/content-types/src/taxonomies/utils.ts diff --git a/packages/user-post-types/tsconfig.json b/packages/content-types/tsconfig.json similarity index 86% rename from packages/user-post-types/tsconfig.json rename to packages/content-types/tsconfig.json index c9ed4857b0a401..3d87d1ee69f232 100644 --- a/packages/user-post-types/tsconfig.json +++ b/packages/content-types/tsconfig.json @@ -5,7 +5,9 @@ "types": [ "style-imports", "react-css-custom-properties" ] }, "references": [ + { "path": "../admin-ui" }, { "path": "../components" }, + { "path": "../compose" }, { "path": "../core-data" }, { "path": "../data" }, { "path": "../dataviews" }, @@ -14,6 +16,7 @@ { "path": "../icons" }, { "path": "../notices" }, { "path": "../private-apis" }, + { "path": "../route" }, { "path": "../ui" }, { "path": "../url" } ] diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts index 62fc967457292a..4d3c3a58bfb4b0 100644 --- a/packages/private-apis/src/implementation.ts +++ b/packages/private-apis/src/implementation.ts @@ -20,6 +20,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/connectors', '@wordpress/workflows', '@wordpress/components', + '@wordpress/content-types', '@wordpress/core-commands', '@wordpress/core-data', '@wordpress/customize-widgets', @@ -47,8 +48,6 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/upload-media', '@wordpress/global-styles-ui', '@wordpress/ui', - '@wordpress/user-post-types', - '@wordpress/user-taxonomies', '@wordpress/views', ]; diff --git a/packages/user-post-types/package.json b/packages/user-post-types/package.json deleted file mode 100644 index 83ae57243da3ab..00000000000000 --- a/packages/user-post-types/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@wordpress/user-post-types", - "version": "1.0.0", - "private": true, - "description": "User-defined post types editor components and data model.", - "author": "The WordPress Contributors", - "license": "GPL-2.0-or-later", - "keywords": [ - "wordpress", - "gutenberg", - "post-types" - ], - "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/user-post-types/README.md", - "repository": { - "type": "git", - "url": "https://github.com/WordPress/gutenberg.git", - "directory": "packages/user-post-types" - }, - "bugs": { - "url": "https://github.com/WordPress/gutenberg/issues" - }, - "module": "build-module/index.mjs", - "exports": { - ".": { - "types": "./build-types/index.d.ts", - "import": "./build-module/index.mjs" - }, - "./package.json": "./package.json" - }, - "wpScriptModuleExports": "./build-module/index.mjs", - "types": "build-types", - "dependencies": { - "@wordpress/components": "file:../components", - "@wordpress/core-data": "file:../core-data", - "@wordpress/data": "file:../data", - "@wordpress/dataviews": "file:../dataviews", - "@wordpress/element": "file:../element", - "@wordpress/i18n": "file:../i18n", - "@wordpress/icons": "file:../icons", - "@wordpress/notices": "file:../notices", - "@wordpress/private-apis": "file:../private-apis", - "@wordpress/ui": "file:../ui", - "@wordpress/url": "file:../url" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/user-post-types/src/index.ts b/packages/user-post-types/src/index.ts deleted file mode 100644 index 9f76879166d370..00000000000000 --- a/packages/user-post-types/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type { - CoreDataError, - PostTypeFormData, - PostTypeRecord, - StoredConfig, - StoredLabels, - SupportFeature, -} from './types'; - -export { - BLANK_RECORD, - DEFAULT_SUPPORTS, - SUPPORT_FEATURES, - serializeForSave, - toFormData, - usePublicTaxonomies, -} from './utils'; - -export * from './fields'; - -export { default as activateAction } from './actions/activate'; -export { default as deactivateAction } from './actions/deactivate'; -export { default as deletePostTypeAction } from './actions/delete'; -export { default as duplicatePostTypeAction } from './actions/duplicate'; -export { createStatusAction, type StatusActionConfig } from './actions/utils'; diff --git a/packages/user-taxonomies/src/index.ts b/packages/user-taxonomies/src/index.ts deleted file mode 100644 index 14d7ce72b2b060..00000000000000 --- a/packages/user-taxonomies/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type { - CoreDataError, - StoredConfig, - StoredLabels, - TaxonomyFormData, - TaxonomyRecord, -} from './types'; - -export { - BLANK_RECORD, - serializeForSave, - toFormData, - usePublicPostTypes, -} from './utils'; - -export * from './fields'; - -export { default as activateAction } from './actions/activate'; -export { default as deactivateAction } from './actions/deactivate'; -export { default as deleteTaxonomyAction } from './actions/delete'; -export { default as duplicateTaxonomyAction } from './actions/duplicate'; -export { createStatusAction, type StatusActionConfig } from './actions/utils'; diff --git a/packages/user-taxonomies/src/lock-unlock.ts b/packages/user-taxonomies/src/lock-unlock.ts deleted file mode 100644 index 9fcd401fb495a1..00000000000000 --- a/packages/user-taxonomies/src/lock-unlock.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * WordPress dependencies - */ -import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; - -export const { lock, unlock } = - __dangerousOptInToUnstableAPIsOnlyForCoreModules( - 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', - '@wordpress/user-taxonomies' - ); diff --git a/packages/user-taxonomies/tsconfig.json b/packages/user-taxonomies/tsconfig.json deleted file mode 100644 index c9ed4857b0a401..00000000000000 --- a/packages/user-taxonomies/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "types": [ "style-imports", "react-css-custom-properties" ] - }, - "references": [ - { "path": "../components" }, - { "path": "../core-data" }, - { "path": "../data" }, - { "path": "../dataviews" }, - { "path": "../element" }, - { "path": "../i18n" }, - { "path": "../icons" }, - { "path": "../notices" }, - { "path": "../private-apis" }, - { "path": "../ui" }, - { "path": "../url" } - ] -} diff --git a/routes/content-types/package.json b/routes/content-types/package.json new file mode 100644 index 00000000000000..bc45f331aa3e9d --- /dev/null +++ b/routes/content-types/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wordpress/content-types-route", + "version": "1.0.0", + "private": true, + "route": { + "path": "/", + "page": [ + "content-types" + ] + }, + "dependencies": { + "@wordpress/content-types": "file:../../packages/content-types", + "@wordpress/i18n": "file:../../packages/i18n", + "@wordpress/route": "file:../../packages/route" + } +} diff --git a/routes/content-types/route.ts b/routes/content-types/route.ts new file mode 100644 index 00000000000000..e7e5fc0a80217a --- /dev/null +++ b/routes/content-types/route.ts @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import { POST_TYPES_PATH } from '@wordpress/content-types'; +import { __ } from '@wordpress/i18n'; +import { redirect } from '@wordpress/route'; + +export const route = { + beforeLoad: () => { + throw redirect( { to: POST_TYPES_PATH } ); + }, + title: () => __( 'Content Types' ), +}; diff --git a/routes/post-type-edit/package.json b/routes/post-type-edit/package.json index c781618658ec3e..421fc888c68893 100644 --- a/routes/post-type-edit/package.json +++ b/routes/post-type-edit/package.json @@ -3,23 +3,18 @@ "version": "1.0.0", "private": true, "route": { - "path": "/edit/$id", + "path": "/post-types/$id", "page": [ - "post-types" + "content-types" ] }, "dependencies": { - "@wordpress/admin-ui": "file:../../packages/admin-ui", - "@wordpress/components": "file:../../packages/components", - "@wordpress/compose": "file:../../packages/compose", + "@wordpress/base-styles": "file:../../packages/base-styles", + "@wordpress/content-types": "file:../../packages/content-types", "@wordpress/core-data": "file:../../packages/core-data", "@wordpress/data": "file:../../packages/data", - "@wordpress/dataviews": "file:../../packages/dataviews", - "@wordpress/element": "file:../../packages/element", "@wordpress/i18n": "file:../../packages/i18n", "@wordpress/notices": "file:../../packages/notices", - "@wordpress/route": "file:../../packages/route", - "@wordpress/ui": "file:../../packages/ui", - "@wordpress/user-post-types": "file:../../packages/user-post-types" + "@wordpress/route": "file:../../packages/route" } } diff --git a/routes/post-type-edit/post-type-form.scss b/routes/post-type-edit/post-type-form.scss deleted file mode 100644 index 9988d1f443e1a6..00000000000000 --- a/routes/post-type-edit/post-type-form.scss +++ /dev/null @@ -1,7 +0,0 @@ -.post-type-form { - box-sizing: border-box; - width: 100%; - max-width: 680px; - margin: 0 auto; - padding: 24px; -} diff --git a/routes/post-type-edit/route.ts b/routes/post-type-edit/route.ts index f61f0de6f5b4cb..01b9a1222c160a 100644 --- a/routes/post-type-edit/route.ts +++ b/routes/post-type-edit/route.ts @@ -1,15 +1,17 @@ /** * WordPress dependencies */ +import { + NEW_ID, + POST_TYPE_ENTITY, + POST_TYPES_PATH, +} from '@wordpress/content-types'; import { dispatch, resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { redirect } from '@wordpress/route'; -const USER_POST_TYPE_POST_TYPE = 'wp_user_post_type'; -const NEW_ID = 'new'; - type RouteArgs = { params: { id: string } }; export const route = { @@ -23,7 +25,7 @@ export const route = { try { record = await resolveSelect( coreStore ).getEntityRecord( 'postType', - USER_POST_TYPE_POST_TYPE, + POST_TYPE_ENTITY, id ); } catch { @@ -35,7 +37,7 @@ export const route = { __( 'Post type not found.' ), { type: 'snackbar' } ); - throw redirect( { throw: true, to: '/' } ); + throw redirect( { throw: true, to: POST_TYPES_PATH } ); } }, title: async ( { params }: RouteArgs ) => { @@ -45,7 +47,7 @@ export const route = { const id = parseInt( params.id, 10 ); const record = ( await resolveSelect( coreStore ).getEntityRecord( 'postType', - USER_POST_TYPE_POST_TYPE, + POST_TYPE_ENTITY, id ) ) as { title?: { raw?: string; rendered?: string } } | null; return ( diff --git a/routes/post-type-edit/stage.tsx b/routes/post-type-edit/stage.tsx index e070c30f853c09..bebf8298e2af2c 100644 --- a/routes/post-type-edit/stage.tsx +++ b/routes/post-type-edit/stage.tsx @@ -1,329 +1,11 @@ /** * WordPress dependencies */ -import { Breadcrumbs, Page } from '@wordpress/admin-ui'; -import { Button } from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; -import { store as coreStore } from '@wordpress/core-data'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { - DataForm, - useFormValidity, - type Field, - type Form, -} from '@wordpress/dataviews'; -import { useMemo, useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { useNavigate, useParams } from '@wordpress/route'; -import { Stack } from '@wordpress/ui'; -import { - addNewField, - addNewItemLabelField, - allItemsField, - archivesField, - attributesField, - BLANK_RECORD, - descriptionField, - editItemField, - featuredImageField, - filterItemsListField, - generalForm, - hasArchiveField, - hierarchicalField, - insertIntoItemField, - itemsListField, - itemsListNavigationField, - labelsForm, - menuNameField, - newItemField, - notFoundField, - notFoundInTrashField, - parentItemColonField, - pluralLabelField, - publicField, - removeFeaturedImageField, - searchItemsField, - serializeForSave, - setFeaturedImageField, - showInRestField, - singularLabelField, - statusField, - supportsField, - toFormData, - uploadedToThisItemField, - useFeaturedImageField, - useSlugField, - useTaxonomiesField, - viewItemField, - viewItemsField, - type PostTypeFormData, - type PostTypeRecord, -} from '@wordpress/user-post-types'; +import { PostTypeEdit } from '@wordpress/content-types'; /** * Internal dependencies */ -import './post-type-form.scss'; +import './style.scss'; -const NEW_ID = 'new'; -const USER_POST_TYPE_POST_TYPE = 'wp_user_post_type'; - -type PostTypePageProps = { - isAddMode: boolean; - initialData: PostTypeFormData; - title: string; - breadcrumbLabel: string; - subTitle: string; - onSaved?: ( saved: PostTypeFormData & { id: number } ) => void; -}; - -function PostTypeEditStage() { - const { id } = useParams( { from: '/edit/$id' } ); - const navigate = useNavigate(); - const isAddMode = id === NEW_ID; - const postTypeId = parseInt( id, 10 ); - const record = useSelect( - ( select ) => { - return ( - ! isAddMode && - // beforeLoad (route.ts) guarantees the record is in cache. - select( coreStore ).getEntityRecord< PostTypeRecord >( - 'postType', - USER_POST_TYPE_POST_TYPE, - postTypeId - )! - ); - }, - [ isAddMode, postTypeId ] - ); - const initialData = - ! isAddMode && record ? toFormData( record ) : BLANK_RECORD; - const title = isAddMode ? __( 'Add post type' ) : initialData.title.raw; - const commonProps = { initialData, title }; - const postTypePageProps: PostTypePageProps = isAddMode - ? { - ...commonProps, - isAddMode: true, - breadcrumbLabel: __( 'Add new' ), - subTitle: __( - 'Define a new post type. Fill in the essentials under General; expand Labels to customize.' - ), - onSaved: ( saved ) => navigate( { to: `/edit/${ saved.id }` } ), - } - : { - ...commonProps, - isAddMode: false, - breadcrumbLabel: title, - subTitle: __( - 'Edit this post type. Expand the Labels section to adjust labels.' - ), - }; - - // key remounts PostTypePage when navigating between records so in-flight - // form state doesn't leak across different post types. - return ; -} - -function PostTypePage( { - isAddMode, - initialData, - title, - breadcrumbLabel, - subTitle, - onSaved, -}: PostTypePageProps ) { - const [ data, setData ] = useState< PostTypeFormData >( initialData ); - const [ isSaving, setIsSaving ] = useState( false ); - const originalSlug = ! isAddMode ? initialData.slug : undefined; - const slugField = useSlugField( originalSlug, data.slug ); - const taxonomiesField = useTaxonomiesField(); - const fields = useMemo< Field< PostTypeFormData >[] >( - () => [ - // General - pluralLabelField, - singularLabelField, - slugField, - descriptionField, - taxonomiesField, - supportsField, - publicField, - hierarchicalField, - hasArchiveField, - showInRestField, - statusField, - // Labels - menuNameField, - allItemsField, - addNewField, - addNewItemLabelField, - editItemField, - newItemField, - viewItemField, - viewItemsField, - searchItemsField, - notFoundField, - notFoundInTrashField, - parentItemColonField, - archivesField, - attributesField, - insertIntoItemField, - uploadedToThisItemField, - featuredImageField, - setFeaturedImageField, - removeFeaturedImageField, - useFeaturedImageField, - filterItemsListField, - itemsListNavigationField, - itemsListField, - ], - [ slugField, taxonomiesField ] - ); - - const form = useMemo< Form >( - () => ( { - layout: { type: 'card', isCollapsible: true }, - fields: [ - { - id: 'general', - label: __( 'General' ), - description: __( - 'Core identity, taxonomies, supports, and activation.' - ), - layout: { - type: 'card', - isCollapsible: true, - isOpened: true, - }, - children: generalForm.fields, - }, - { - id: 'labels', - label: __( 'Labels' ), - description: __( - 'Override the text WordPress shows in admin lists, menus, and forms. Leave blank to use defaults derived from the plural and singular names.' - ), - layout: { - type: 'card', - isCollapsible: true, - isOpened: false, - }, - children: labelsForm.fields, - }, - ], - } ), - [] - ); - - const { validity, isValid } = useFormValidity( data, fields, form ); - - const formId = useInstanceId( PostTypePage, 'post-type-form' ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function onSave() { - if ( isSaving || ! isValid ) { - return; - } - setIsSaving( true ); - try { - const saved = ( await saveEntityRecord( - 'postType', - USER_POST_TYPE_POST_TYPE, - serializeForSave( data ), - { throwOnError: true } - ) ) as { id: number } | undefined; - const successMessage = isAddMode - ? sprintf( - /* translators: %s: post type plural label. */ - __( '"%s" post type created.' ), - data.title.raw - ) - : sprintf( - /* translators: %s: post type plural label. */ - __( '"%s" post type updated.' ), - data.title.raw - ); - createSuccessNotice( successMessage, { type: 'snackbar' } ); - if ( saved?.id !== undefined ) { - onSaved?.( { ...data, id: saved.id } ); - } - } catch ( error: any ) { - let errorMessage: string; - if ( error?.message && error?.code !== 'unknown_error' ) { - errorMessage = error.message; - } else if ( isAddMode ) { - errorMessage = __( 'Failed to create post type.' ); - } else { - errorMessage = __( 'Failed to update post type.' ); - } - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } finally { - setIsSaving( false ); - } - } - - return ( - - } - subTitle={ subTitle } - actions={ - - } - > - { - event.preventDefault(); - onSave(); - } } - /> - } - > - - data={ data } - fields={ fields } - form={ form } - validity={ validity } - onChange={ ( edits ) => - setData( - ( prev ) => - ( { - ...prev, - ...edits, - } ) as PostTypeFormData - ) - } - /> - - - ); -} - -export const stage = PostTypeEditStage; +export const stage = PostTypeEdit; diff --git a/routes/post-type-edit/style.scss b/routes/post-type-edit/style.scss new file mode 100644 index 00000000000000..34f6cb27780048 --- /dev/null +++ b/routes/post-type-edit/style.scss @@ -0,0 +1 @@ +@use "@wordpress/content-types/build-style/style.css" as content-types; diff --git a/routes/post-types-list/package.json b/routes/post-types-list/package.json new file mode 100644 index 00000000000000..6d425f93ac0993 --- /dev/null +++ b/routes/post-types-list/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wordpress/post-types-list-route", + "version": "1.0.0", + "private": true, + "route": { + "path": "/post-types", + "page": [ + "content-types" + ] + }, + "dependencies": { + "@wordpress/base-styles": "file:../../packages/base-styles", + "@wordpress/content-types": "file:../../packages/content-types", + "@wordpress/i18n": "file:../../packages/i18n" + } +} diff --git a/routes/post-types/route.ts b/routes/post-types-list/route.ts similarity index 100% rename from routes/post-types/route.ts rename to routes/post-types-list/route.ts diff --git a/routes/post-types-list/stage.tsx b/routes/post-types-list/stage.tsx new file mode 100644 index 00000000000000..864fc386b579d5 --- /dev/null +++ b/routes/post-types-list/stage.tsx @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { Layout, PostTypesList } from '@wordpress/content-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +function Stage() { + return ( + + + + ); +} + +export const stage = Stage; diff --git a/routes/post-types-list/style.scss b/routes/post-types-list/style.scss new file mode 100644 index 00000000000000..34f6cb27780048 --- /dev/null +++ b/routes/post-types-list/style.scss @@ -0,0 +1 @@ +@use "@wordpress/content-types/build-style/style.css" as content-types; diff --git a/routes/post-types/actions/index.ts b/routes/post-types/actions/index.ts deleted file mode 100644 index e9f9942313c5d0..00000000000000 --- a/routes/post-types/actions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useEditPostTypeAction } from './edit'; -export { default as quickEditPostTypeAction } from './quick-edit'; diff --git a/routes/post-types/package.json b/routes/post-types/package.json deleted file mode 100644 index e32b442830f05e..00000000000000 --- a/routes/post-types/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@wordpress/post-types-route", - "version": "1.0.0", - "private": true, - "route": { - "path": "/", - "page": [ - "post-types" - ] - }, - "dependencies": { - "@wordpress/admin-ui": "file:../../packages/admin-ui", - "@wordpress/base-styles": "file:../../packages/base-styles", - "@wordpress/components": "file:../../packages/components", - "@wordpress/core-data": "file:../../packages/core-data", - "@wordpress/data": "file:../../packages/data", - "@wordpress/dataviews": "file:../../packages/dataviews", - "@wordpress/element": "file:../../packages/element", - "@wordpress/i18n": "file:../../packages/i18n", - "@wordpress/icons": "file:../../packages/icons", - "@wordpress/notices": "file:../../packages/notices", - "@wordpress/route": "file:../../packages/route", - "@wordpress/ui": "file:../../packages/ui", - "@wordpress/user-post-types": "file:../../packages/user-post-types" - } -} diff --git a/routes/post-types/style.scss b/routes/post-types/style.scss deleted file mode 100644 index ce2b0276589d00..00000000000000 --- a/routes/post-types/style.scss +++ /dev/null @@ -1,94 +0,0 @@ -@use "@wordpress/base-styles/variables" as *; -@use "@wordpress/base-styles/colors" as *; -@use "@wordpress/base-styles/mixins" as *; - -// Size the stage to the viewport so DataViews owns its own scroll and -// the sticky footer stays visible. Note: when the wp-admin sidebar -// (#adminmenu) is taller than the viewport, #wpwrap still grows to -// fit it, so empty space may appear below DataViews. -.boot-layout__stage:has(.post-types-page) { - height: calc(100vh - #{$admin-bar-height-big}); - - @include break-medium { - height: calc(100vh - #{$admin-bar-height}); - } -} - -.dataviews-action-modal__quick-edit-post-type { - justify-content: flex-end; - align-items: stretch; - - .components-modal__frame { - margin: $grid-unit-20 $grid-unit-20 $grid-unit-20 0; - height: calc(100% - #{$grid-unit-20 * 2}); - max-height: calc(100% - #{$grid-unit-20 * 2}); - max-width: 400px; - border-radius: $radius-large; - position: relative; - animation-name: none; - - @media (prefers-reduced-motion: no-preference) { - animation-name: quick-edit-post-type-slide-in-right; - animation-duration: 0.2s; - animation-timing-function: ease-out; - } - } - - &.is-animating-out .components-modal__frame { - animation-name: none; - - @media (prefers-reduced-motion: no-preference) { - animation-name: quick-edit-post-type-slide-out-right; - animation-duration: 0.2s; - animation-timing-function: ease-out; - } - } - - .components-modal__content { - padding: 0; - overflow-y: auto; - } - - .dataviews-action-modal__quick-edit-post-type-header { - position: sticky; - top: 0; - z-index: 1; - background: $white; - padding: $grid-unit-20 $grid-unit-30; - } - - .dataviews-action-modal__quick-edit-post-type-content { - padding: 0 $grid-unit-30; - } - - .dataviews-action-modal__quick-edit-post-type-footer { - position: sticky; - bottom: 0; - z-index: 1; - background: $white; - padding: $grid-unit-20 $grid-unit-30; - - .components-button { - flex: 1; - justify-content: center; - } - } -} - -@keyframes quick-edit-post-type-slide-in-right { - from { - transform: translateX(100%); - } - to { - transform: translateX(0); - } -} - -@keyframes quick-edit-post-type-slide-out-right { - from { - transform: translateX(0); - } - to { - transform: translateX(100%); - } -} diff --git a/routes/taxonomies-list/package.json b/routes/taxonomies-list/package.json new file mode 100644 index 00000000000000..1f95d081fa5491 --- /dev/null +++ b/routes/taxonomies-list/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wordpress/taxonomies-list-route", + "version": "1.0.0", + "private": true, + "route": { + "path": "/taxonomies", + "page": [ + "content-types" + ] + }, + "dependencies": { + "@wordpress/base-styles": "file:../../packages/base-styles", + "@wordpress/content-types": "file:../../packages/content-types", + "@wordpress/i18n": "file:../../packages/i18n" + } +} diff --git a/routes/taxonomies/route.ts b/routes/taxonomies-list/route.ts similarity index 100% rename from routes/taxonomies/route.ts rename to routes/taxonomies-list/route.ts diff --git a/routes/taxonomies-list/stage.tsx b/routes/taxonomies-list/stage.tsx new file mode 100644 index 00000000000000..d965cc2110b891 --- /dev/null +++ b/routes/taxonomies-list/stage.tsx @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { Layout, TaxonomiesList } from '@wordpress/content-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +function Stage() { + return ( + + + + ); +} + +export const stage = Stage; diff --git a/routes/taxonomies-list/style.scss b/routes/taxonomies-list/style.scss new file mode 100644 index 00000000000000..34f6cb27780048 --- /dev/null +++ b/routes/taxonomies-list/style.scss @@ -0,0 +1 @@ +@use "@wordpress/content-types/build-style/style.css" as content-types; diff --git a/routes/taxonomies/actions/index.ts b/routes/taxonomies/actions/index.ts deleted file mode 100644 index ea02296c27a915..00000000000000 --- a/routes/taxonomies/actions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useEditTaxonomyAction } from './edit'; -export { default as quickEditTaxonomyAction } from './quick-edit'; diff --git a/routes/taxonomies/package.json b/routes/taxonomies/package.json deleted file mode 100644 index 0ecc0ef1bf2cbc..00000000000000 --- a/routes/taxonomies/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@wordpress/taxonomies-route", - "version": "1.0.0", - "private": true, - "route": { - "path": "/", - "page": [ - "taxonomies" - ] - }, - "dependencies": { - "@wordpress/admin-ui": "file:../../packages/admin-ui", - "@wordpress/base-styles": "file:../../packages/base-styles", - "@wordpress/components": "file:../../packages/components", - "@wordpress/core-data": "file:../../packages/core-data", - "@wordpress/data": "file:../../packages/data", - "@wordpress/dataviews": "file:../../packages/dataviews", - "@wordpress/element": "file:../../packages/element", - "@wordpress/i18n": "file:../../packages/i18n", - "@wordpress/icons": "file:../../packages/icons", - "@wordpress/notices": "file:../../packages/notices", - "@wordpress/route": "file:../../packages/route", - "@wordpress/ui": "file:../../packages/ui", - "@wordpress/user-taxonomies": "file:../../packages/user-taxonomies" - } -} diff --git a/routes/taxonomy-edit/package.json b/routes/taxonomy-edit/package.json index a3c48c054c34c3..39d8d5535726a7 100644 --- a/routes/taxonomy-edit/package.json +++ b/routes/taxonomy-edit/package.json @@ -3,24 +3,18 @@ "version": "1.0.0", "private": true, "route": { - "path": "/edit/$id", + "path": "/taxonomies/$id", "page": [ - "taxonomies" + "content-types" ] }, "dependencies": { - "@wordpress/admin-ui": "file:../../packages/admin-ui", "@wordpress/base-styles": "file:../../packages/base-styles", - "@wordpress/components": "file:../../packages/components", - "@wordpress/compose": "file:../../packages/compose", + "@wordpress/content-types": "file:../../packages/content-types", "@wordpress/core-data": "file:../../packages/core-data", "@wordpress/data": "file:../../packages/data", - "@wordpress/dataviews": "file:../../packages/dataviews", - "@wordpress/element": "file:../../packages/element", "@wordpress/i18n": "file:../../packages/i18n", "@wordpress/notices": "file:../../packages/notices", - "@wordpress/route": "file:../../packages/route", - "@wordpress/ui": "file:../../packages/ui", - "@wordpress/user-taxonomies": "file:../../packages/user-taxonomies" + "@wordpress/route": "file:../../packages/route" } } diff --git a/routes/taxonomy-edit/route.ts b/routes/taxonomy-edit/route.ts index 087a4cd064f1c4..e8f15777a5bb57 100644 --- a/routes/taxonomy-edit/route.ts +++ b/routes/taxonomy-edit/route.ts @@ -1,15 +1,17 @@ /** * WordPress dependencies */ +import { + NEW_ID, + TAXONOMIES_PATH, + TAXONOMY_ENTITY, +} from '@wordpress/content-types'; import { dispatch, resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { redirect } from '@wordpress/route'; -const USER_TAXONOMY_POST_TYPE = 'wp_user_taxonomy'; -const NEW_ID = 'new'; - type RouteArgs = { params: { id: string } }; export const route = { @@ -23,7 +25,7 @@ export const route = { try { record = await resolveSelect( coreStore ).getEntityRecord( 'postType', - USER_TAXONOMY_POST_TYPE, + TAXONOMY_ENTITY, id ); } catch { @@ -35,7 +37,7 @@ export const route = { __( 'Taxonomy not found.' ), { type: 'snackbar' } ); - throw redirect( { throw: true, to: '/' } ); + throw redirect( { throw: true, to: TAXONOMIES_PATH } ); } }, title: async ( { params }: RouteArgs ) => { @@ -45,7 +47,7 @@ export const route = { const id = parseInt( params.id, 10 ); const record = ( await resolveSelect( coreStore ).getEntityRecord( 'postType', - USER_TAXONOMY_POST_TYPE, + TAXONOMY_ENTITY, id ) ) as { title?: { raw?: string; rendered?: string } } | null; return ( diff --git a/routes/taxonomy-edit/stage.tsx b/routes/taxonomy-edit/stage.tsx index 4c27ab3c3c5fc6..3c6d6d3ca4454b 100644 --- a/routes/taxonomy-edit/stage.tsx +++ b/routes/taxonomy-edit/stage.tsx @@ -1,339 +1,11 @@ /** * WordPress dependencies */ -import { Breadcrumbs, Page } from '@wordpress/admin-ui'; -import { Button } from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; -import { store as coreStore } from '@wordpress/core-data'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { - DataForm, - useFormValidity, - type Field, - type Form, -} from '@wordpress/dataviews'; -import { useMemo, useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { useNavigate, useParams } from '@wordpress/route'; -import { Stack } from '@wordpress/ui'; -import { - addNewItemLabelField, - addOrRemoveItemsField, - allItemsField, - backToItemsField, - BLANK_RECORD, - chooseFromMostUsedField, - descriptionField, - editItemField, - generalFormFields, - hierarchicalField, - labelsActionsField, - labelsFormFields, - menuNameField, - newItemNameField, - notFoundField, - parentItemColonField, - parentItemField, - pluralLabelField, - popularItemsField, - publicField, - publiclyQueryableField, - searchItemsField, - separateItemsField, - serializeForSave, - showAdminColumnField, - showInMenuField, - showInNavMenusField, - showInQuickEditField, - showInRestField, - showTagcloudField, - showUiField, - singularLabelField, - statusField, - toFormData, - updateItemField, - useObjectTypeField, - useSlugField, - viewItemField, - visibilityFormFields, - type TaxonomyFormData, - type TaxonomyRecord, -} from '@wordpress/user-taxonomies'; +import { TaxonomyEdit } from '@wordpress/content-types'; /** * Internal dependencies */ -import './taxonomy-form.scss'; +import './style.scss'; -const NEW_ID = 'new'; -const USER_TAXONOMY_POST_TYPE = 'wp_user_taxonomy'; - -type TaxonomyPageProps = { - isAddMode: boolean; - initialData: TaxonomyFormData; - title: string; - breadcrumbLabel: string; - subTitle: string; - onSaved?: ( saved: TaxonomyFormData & { id: number } ) => void; -}; - -function TaxonomyEditStage() { - const { id } = useParams( { from: '/edit/$id' } ); - const navigate = useNavigate(); - const isAddMode = id === NEW_ID; - const taxonomyId = parseInt( id, 10 ); - const record = useSelect( - ( select ) => { - return ( - ! isAddMode && - // beforeLoad (route.ts) guarantees the record is in cache. - select( coreStore ).getEntityRecord< TaxonomyRecord >( - 'postType', - USER_TAXONOMY_POST_TYPE, - taxonomyId - )! - ); - }, - [ isAddMode, taxonomyId ] - ); - const initialData = - ! isAddMode && record ? toFormData( record ) : BLANK_RECORD; - const title = isAddMode ? __( 'Add taxonomy' ) : initialData.title.raw; - const commonProps = { initialData, title }; - const taxonomyPageProps: TaxonomyPageProps = isAddMode - ? { - ...commonProps, - isAddMode: true, - breadcrumbLabel: __( 'Add new' ), - subTitle: __( - 'Define a new taxonomy. Fill in the essentials under General; expand Labels to customize.' - ), - onSaved: ( saved ) => navigate( { to: `/edit/${ saved.id }` } ), - } - : { - ...commonProps, - isAddMode: false, - breadcrumbLabel: title, - subTitle: __( - 'Edit this taxonomy. Expand the Labels section to adjust labels.' - ), - }; - - // key remounts TaxonomyPage when navigating between records so in-flight - // form state doesn't leak across different taxonomies. - return ; -} - -function TaxonomyPage( { - isAddMode, - initialData, - title, - breadcrumbLabel, - subTitle, - onSaved, -}: TaxonomyPageProps ) { - const [ data, setData ] = useState< TaxonomyFormData >( initialData ); - const [ isSaving, setIsSaving ] = useState( false ); - const originalSlug = ! isAddMode ? initialData.slug : undefined; - const slugField = useSlugField( originalSlug, data.slug ); - const objectTypeField = useObjectTypeField(); - const fields = useMemo< Field< TaxonomyFormData >[] >( - () => [ - // General - pluralLabelField, - singularLabelField, - slugField, - descriptionField, - objectTypeField, - hierarchicalField, - statusField, - // Visibility - publicField, - showInRestField, - publiclyQueryableField, - showUiField, - showInMenuField, - showInQuickEditField, - showAdminColumnField, - showInNavMenusField, - showTagcloudField, - // Labels - labelsActionsField, - menuNameField, - allItemsField, - editItemField, - viewItemField, - updateItemField, - addNewItemLabelField, - newItemNameField, - searchItemsField, - notFoundField, - backToItemsField, - parentItemField, - popularItemsField, - separateItemsField, - parentItemColonField, - addOrRemoveItemsField, - chooseFromMostUsedField, - ], - [ slugField, objectTypeField ] - ); - - const form = useMemo< Form >( - () => ( { - layout: { type: 'card', isCollapsible: true }, - fields: [ - { - id: 'general', - label: __( 'General' ), - description: __( - 'Core identity, post types, and activation.' - ), - layout: { - type: 'card', - isCollapsible: true, - isOpened: true, - }, - children: generalFormFields, - }, - { - id: 'visibility', - label: __( 'Visibility' ), - description: __( - 'Where this taxonomy appears: REST API, admin UI, and front-end surfaces.' - ), - layout: { - type: 'card', - isCollapsible: true, - isOpened: false, - }, - children: visibilityFormFields, - }, - { - id: 'labels', - label: __( 'Labels' ), - layout: { - type: 'card', - isCollapsible: true, - isOpened: false, - }, - children: labelsFormFields, - }, - ], - } ), - [] - ); - - const { validity, isValid } = useFormValidity( data, fields, form ); - - const formId = useInstanceId( TaxonomyPage, 'taxonomy-form' ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function onSave() { - if ( isSaving || ! isValid ) { - return; - } - setIsSaving( true ); - try { - const saved = ( await saveEntityRecord( - 'postType', - USER_TAXONOMY_POST_TYPE, - serializeForSave( data ), - { throwOnError: true } - ) ) as { id: number } | undefined; - const successMessage = isAddMode - ? sprintf( - /* translators: %s: taxonomy plural label. */ - __( '"%s" taxonomy created.' ), - data.title.raw - ) - : sprintf( - /* translators: %s: taxonomy plural label. */ - __( '"%s" taxonomy updated.' ), - data.title.raw - ); - createSuccessNotice( successMessage, { type: 'snackbar' } ); - if ( saved?.id !== undefined ) { - onSaved?.( { ...data, id: saved.id } ); - } - } catch ( error: any ) { - let errorMessage: string; - if ( error?.message && error?.code !== 'unknown_error' ) { - errorMessage = error.message; - } else if ( isAddMode ) { - errorMessage = __( 'Failed to create taxonomy.' ); - } else { - errorMessage = __( 'Failed to update taxonomy.' ); - } - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } finally { - setIsSaving( false ); - } - } - - return ( - - } - subTitle={ subTitle } - actions={ - - } - > - { - event.preventDefault(); - onSave(); - } } - /> - } - > - - data={ data } - fields={ fields } - form={ form } - validity={ validity } - onChange={ ( edits ) => - setData( - ( prev ) => - ( { - ...prev, - ...edits, - } ) as TaxonomyFormData - ) - } - /> - - - ); -} - -export const stage = TaxonomyEditStage; +export const stage = TaxonomyEdit; diff --git a/routes/taxonomy-edit/style.scss b/routes/taxonomy-edit/style.scss new file mode 100644 index 00000000000000..34f6cb27780048 --- /dev/null +++ b/routes/taxonomy-edit/style.scss @@ -0,0 +1 @@ +@use "@wordpress/content-types/build-style/style.css" as content-types; diff --git a/routes/taxonomy-edit/taxonomy-form.scss b/routes/taxonomy-edit/taxonomy-form.scss deleted file mode 100644 index f2367deadb24b7..00000000000000 --- a/routes/taxonomy-edit/taxonomy-form.scss +++ /dev/null @@ -1,13 +0,0 @@ -@use "@wordpress/base-styles/colors" as *; - -.taxonomy-form { - box-sizing: border-box; - width: 100%; - max-width: 680px; - margin: 0 auto; - padding: 24px; -} - -.taxonomy-form__help-text { - color: $gray-700; -} diff --git a/test/e2e/specs/admin/user-post-types.spec.js b/test/e2e/specs/admin/user-post-types.spec.js index f818b1cff49a3e..2aa7ceac39298f 100644 --- a/test/e2e/specs/admin/user-post-types.spec.js +++ b/test/e2e/specs/admin/user-post-types.spec.js @@ -4,7 +4,7 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); const SETTINGS_PAGE_PATH = 'options-general.php'; -const POST_TYPES_PAGE_QUERY = 'page=post-types-wp-admin'; +const CONTENT_TYPES_PAGE_QUERY = 'page=content-types-wp-admin&p=/post-types'; const POST_TYPES_REST_BASE = 'user-post-types'; async function createUserPostType( requestUtils ) { @@ -35,7 +35,10 @@ test.describe( 'User post types', () => { } ); test( 'creates a post type and registers it', async ( { admin, page } ) => { - await admin.visitAdminPage( SETTINGS_PAGE_PATH, POST_TYPES_PAGE_QUERY ); + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONTENT_TYPES_PAGE_QUERY + ); await page.getByRole( 'button', { name: 'Add post type' } ).click(); @@ -81,7 +84,10 @@ test.describe( 'User post types', () => { requestUtils, } ) => { await createUserPostType( requestUtils ); - await admin.visitAdminPage( SETTINGS_PAGE_PATH, POST_TYPES_PAGE_QUERY ); + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONTENT_TYPES_PAGE_QUERY + ); await page .getByRole( 'row', { name: 'Books' } ) @@ -101,7 +107,10 @@ test.describe( 'User post types', () => { await admin.visitAdminPage( 'edit.php', 'post_type=book' ); await expect( page.getByText( 'Invalid post type.' ) ).toBeVisible(); - await admin.visitAdminPage( SETTINGS_PAGE_PATH, POST_TYPES_PAGE_QUERY ); + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONTENT_TYPES_PAGE_QUERY + ); await page .getByRole( 'row', { name: 'Books' } ) .getByRole( 'button', { name: 'Actions' } ) @@ -130,7 +139,7 @@ test.describe( 'User post types', () => { const created = await createUserPostType( requestUtils ); await admin.visitAdminPage( SETTINGS_PAGE_PATH, - `${ POST_TYPES_PAGE_QUERY }&p=/edit/${ created.id }` + `page=content-types-wp-admin&p=/post-types/${ created.id }` ); await page diff --git a/test/e2e/specs/admin/user-taxonomies.spec.js b/test/e2e/specs/admin/user-taxonomies.spec.js index 81e1f4326faf6a..8b3f2a4f0ed87c 100644 --- a/test/e2e/specs/admin/user-taxonomies.spec.js +++ b/test/e2e/specs/admin/user-taxonomies.spec.js @@ -4,7 +4,7 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); const SETTINGS_PAGE_PATH = 'options-general.php'; -const TAXONOMIES_PAGE_QUERY = 'page=taxonomies-wp-admin'; +const CONTENT_TYPES_PAGE_QUERY = 'page=content-types-wp-admin&p=/taxonomies'; const TAXONOMIES_REST_BASE = 'user-taxonomies'; // Seeds all visibility booleans so the form's `toFormData` reads each toggle @@ -52,7 +52,10 @@ test.describe( 'User taxonomies', () => { admin, page, } ) => { - await admin.visitAdminPage( SETTINGS_PAGE_PATH, TAXONOMIES_PAGE_QUERY ); + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONTENT_TYPES_PAGE_QUERY + ); await page.getByRole( 'button', { name: 'Add taxonomy' } ).click(); @@ -113,7 +116,10 @@ test.describe( 'User taxonomies', () => { requestUtils, } ) => { await createUserTaxonomy( requestUtils ); - await admin.visitAdminPage( SETTINGS_PAGE_PATH, TAXONOMIES_PAGE_QUERY ); + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONTENT_TYPES_PAGE_QUERY + ); await page .getByRole( 'row', { name: 'Genres' } ) @@ -136,7 +142,10 @@ test.describe( 'User taxonomies', () => { ); await expect( page.getByText( 'Invalid taxonomy.' ) ).toBeVisible(); - await admin.visitAdminPage( SETTINGS_PAGE_PATH, TAXONOMIES_PAGE_QUERY ); + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONTENT_TYPES_PAGE_QUERY + ); await page .getByRole( 'row', { name: 'Genres' } ) .getByRole( 'button', { name: 'Actions' } ) @@ -164,7 +173,7 @@ test.describe( 'User taxonomies', () => { const created = await createUserTaxonomy( requestUtils ); await admin.visitAdminPage( SETTINGS_PAGE_PATH, - `${ TAXONOMIES_PAGE_QUERY }&p=/edit/${ created.id }` + `page=content-types-wp-admin&p=/taxonomies/${ created.id }` ); } ); diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index 733e39f8ec36e1..bf65f223980395 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -1734,12 +1734,7 @@ "count": 1 } }, - "routes/post-types/actions/quick-edit.tsx": { - "@wordpress/no-non-module-stylesheet-imports": { - "count": 1 - } - }, - "routes/post-types/stage.tsx": { + "routes/post-types-list/stage.tsx": { "@wordpress/no-non-module-stylesheet-imports": { "count": 1 } @@ -1752,12 +1747,7 @@ "count": 1 } }, - "routes/taxonomies/actions/quick-edit.tsx": { - "@wordpress/no-non-module-stylesheet-imports": { - "count": 1 - } - }, - "routes/taxonomies/stage.tsx": { + "routes/taxonomies-list/stage.tsx": { "@wordpress/no-non-module-stylesheet-imports": { "count": 1 } diff --git a/tsconfig.json b/tsconfig.json index b28050f735aafa..13f15984163b27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ { "path": "packages/boot" }, { "path": "packages/components" }, { "path": "packages/connectors" }, + { "path": "packages/content-types" }, { "path": "packages/compose" }, { "path": "packages/core-abilities" }, { "path": "packages/core-data" }, @@ -81,8 +82,6 @@ { "path": "packages/undo-manager" }, { "path": "packages/upload-media" }, { "path": "packages/url" }, - { "path": "packages/user-post-types" }, - { "path": "packages/user-taxonomies" }, { "path": "packages/viewport" }, { "path": "packages/views" }, { "path": "packages/vips" },