+ 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