diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx
index 0b3512714e14a..c80591caee255 100644
--- a/packages/dataviews/src/components/dataviews-view-config/index.tsx
+++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx
@@ -1,7 +1,8 @@
/**
* External dependencies
*/
-import type { ChangeEvent } from 'react';
+import type { ChangeEvent, ReactNode } from 'react';
+import clsx from 'clsx';
/**
* WordPress dependencies
@@ -26,7 +27,7 @@ import {
Icon,
} from '@wordpress/components';
import { __, _x, sprintf } from '@wordpress/i18n';
-import { memo, useContext, useMemo } from '@wordpress/element';
+import { memo, useContext, useMemo, useState } from '@wordpress/element';
import {
chevronDown,
chevronUp,
@@ -34,6 +35,7 @@ import {
seen,
unseen,
lock,
+ moreVertical,
} from '@wordpress/icons';
import warning from '@wordpress/warning';
import { useInstanceId } from '@wordpress/compose';
@@ -253,8 +255,66 @@ function ItemsPerPageControl() {
);
}
+function PreviewOptions( {
+ previewOptions,
+ onChangePreviewOption,
+ onMenuOpenChange,
+ activeOption,
+}: {
+ previewOptions?: Array< { label: string; id: string } >;
+ onChangePreviewOption?: ( newPreviewOption: string ) => void;
+ onMenuOpenChange: ( isOpen: boolean ) => void;
+ activeOption?: string;
+} ) {
+ const focusPreviewOptionsField = ( id: string ) => {
+ // Focus the visibility button to avoid focus loss.
+ // Our code is safe against the component being unmounted, so we don't need to worry about cleaning the timeout.
+ // eslint-disable-next-line @wordpress/react-no-unsafe-timeout
+ setTimeout( () => {
+ const element = document.querySelector(
+ `.dataviews-field-control__field-${ id } .dataviews-field-control__field-preview-options-button`
+ );
+ if ( element instanceof HTMLElement ) {
+ element.focus();
+ }
+ }, 50 );
+ };
+ return (
+
+ );
+}
function FieldItem( {
field,
+ label,
+ description,
isVisible,
isFirst,
isLast,
@@ -262,8 +322,12 @@ function FieldItem( {
onToggleVisibility,
onMoveUp,
onMoveDown,
+ previewOptions,
+ onChangePreviewOption,
}: {
field: NormalizedField< any >;
+ label?: string;
+ description?: string;
isVisible: boolean;
isFirst?: boolean;
isLast?: boolean;
@@ -271,7 +335,12 @@ function FieldItem( {
onToggleVisibility?: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
+ previewOptions?: Array< { label: string; id: string } >;
+ onChangePreviewOption?: ( newPreviewOption: string ) => void;
} ) {
+ const [ isChangingPreviewOption, setIsChangingPreviewOption ] =
+ useState< boolean >( false );
+
const focusVisibilityField = () => {
// Focus the visibility button to avoid focus loss.
// Our code is safe against the component being unmounted, so we don't need to worry about cleaning the timeout.
@@ -290,7 +359,17 @@ function FieldItem( {
-
@@ -298,8 +377,15 @@ function FieldItem( {
) }
-
- { field.label }
+
+
+ { label || field.label }
+
+ { description && (
+
+ { description }
+
+ ) }
) }
+ { previewOptions && (
+
+ ) }
@@ -461,7 +555,8 @@ function FieldControl() {
const hiddenFields = fields.filter(
( f ) =>
! visibleFieldIds.includes( f.id ) &&
- ! togglableFields.includes( f.id )
+ ! togglableFields.includes( f.id ) &&
+ f.type !== 'media'
);
const visibleFields = visibleFieldIds
.map( ( fieldId ) => fields.find( ( f ) => f.id === fieldId ) )
@@ -471,18 +566,50 @@ function FieldControl() {
return null;
}
const titleField = fields.find( ( f ) => f.id === view.titleField );
- const mediaField = fields.find( ( f ) => f.id === view.mediaField );
+ const previewField = fields.find( ( f ) => f.id === view.mediaField );
const descriptionField = fields.find(
( f ) => f.id === view.descriptionField
);
+
+ const previewFields = fields.filter( ( f ) => f.type === 'media' );
+
+ let previewFieldUI;
+ if ( previewFields.length > 1 ) {
+ const isPreviewFieldVisible =
+ isDefined( previewField ) && ( view.showMedia ?? true );
+ previewFieldUI = isDefined( previewField ) && (
+ {
+ onChangeView( {
+ ...view,
+ showMedia: ! isPreviewFieldVisible,
+ } );
+ } }
+ canMove={ false }
+ previewOptions={ previewFields.map( ( field ) => ( {
+ label: field.label,
+ id: field.id,
+ } ) ) }
+ onChangePreviewOption={ ( newPreviewId ) =>
+ onChangeView( { ...view, mediaField: newPreviewId } )
+ }
+ />
+ );
+ }
const lockedFields = [
{
field: titleField,
isVisibleFlag: 'showTitle',
},
{
- field: mediaField,
+ field: previewField,
isVisibleFlag: 'showMedia',
+ ui: previewFieldUI,
},
{
field: descriptionField,
@@ -493,12 +620,20 @@ function FieldControl() {
( { field, isVisibleFlag } ) =>
// @ts-expect-error
isDefined( field ) && ( view[ isVisibleFlag ] ?? true )
- ) as Array< { field: NormalizedField< any >; isVisibleFlag: string } >;
+ ) as Array< {
+ field: NormalizedField< any >;
+ isVisibleFlag: string;
+ ui?: ReactNode;
+ } >;
const hiddenLockedFields = lockedFields.filter(
( { field, isVisibleFlag } ) =>
// @ts-expect-error
isDefined( field ) && ! ( view[ isVisibleFlag ] ?? true )
- ) as Array< { field: NormalizedField< any >; isVisibleFlag: string } >;
+ ) as Array< {
+ field: NormalizedField< any >;
+ isVisibleFlag: string;
+ ui?: ReactNode;
+ } >;
return (
@@ -507,20 +642,22 @@ function FieldControl() {
!! visibleFields?.length ) && (
{ visibleLockedFields.map(
- ( { field, isVisibleFlag } ) => {
+ ( { field, isVisibleFlag, ui } ) => {
return (
- {
- onChangeView( {
- ...view,
- [ isVisibleFlag ]: false,
- } );
- } }
- canMove={ false }
- />
+ ui ?? (
+ {
+ onChangeView( {
+ ...view,
+ [ isVisibleFlag ]: false,
+ } );
+ } }
+ canMove={ false }
+ />
+ )
);
}
) }
@@ -550,20 +687,23 @@ function FieldControl() {
{ hiddenLockedFields.length > 0 &&
hiddenLockedFields.map(
- ( { field, isVisibleFlag } ) => {
+ ( { field, isVisibleFlag, ui } ) => {
return (
- {
- onChangeView( {
- ...view,
- [ isVisibleFlag ]: true,
- } );
- } }
- canMove={ false }
- />
+ ui ?? (
+ {
+ onChangeView( {
+ ...view,
+ [ isVisibleFlag ]:
+ true,
+ } );
+ } }
+ canMove={ false }
+ />
+ )
);
}
) }
diff --git a/packages/dataviews/src/components/dataviews-view-config/style.scss b/packages/dataviews/src/components/dataviews-view-config/style.scss
index 692dddfb7a90b..fc38e345ec4ce 100644
--- a/packages/dataviews/src/components/dataviews-view-config/style.scss
+++ b/packages/dataviews/src/components/dataviews-view-config/style.scss
@@ -68,7 +68,8 @@
}
.dataviews-field-control__field:hover,
-.dataviews-field-control__field:focus-within {
+.dataviews-field-control__field:focus-within,
+.dataviews-field-control__field.is-interacting {
.dataviews-field-control__actions {
position: unset;
top: unset;
@@ -80,6 +81,18 @@
width: $icon-size;
}
-.dataviews-field-control__label {
+.dataviews-field-control__label-sub-label-container {
flex-grow: 1;
}
+
+.dataviews-field-control__label {
+ display: block;
+}
+
+.dataviews-field-control__sub-label {
+ margin-top: $grid-unit-10;
+ margin-bottom: 0;
+ font-size: 11px;
+ font-style: normal;
+ color: $gray-700;
+}
diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts
index 820f75364df20..8ea13ed0b459c 100644
--- a/packages/dataviews/src/types.ts
+++ b/packages/dataviews/src/types.ts
@@ -42,7 +42,7 @@ export type Operator =
| 'isAll'
| 'isNotAll';
-export type FieldType = 'text' | 'integer' | 'datetime';
+export type FieldType = 'text' | 'integer' | 'datetime' | 'media';
export type ValidationContext = {
elements?: Option[];
diff --git a/packages/editor/README.md b/packages/editor/README.md
index c006ec097982c..3119f3f289637 100644
--- a/packages/editor/README.md
+++ b/packages/editor/README.md
@@ -379,7 +379,7 @@ _Parameters_
- _props.post_ `[Object]`: The post object to edit. This is required.
- _props.\_\_unstableTemplate_ `[Object]`: The template object wrapper the edited post. This is optional and can only be used when the post type supports templates (like posts and pages).
- _props.settings_ `[Object]`: The settings object to use for the editor. This is optional and can be used to override the default settings.
-- _props.children_ `[Element]`: Children elements for which the BlockEditorProvider context should apply. This is optional.
+- _props.children_ `[React.ReactNode]`: Children elements for which the BlockEditorProvider context should apply. This is optional.
_Returns_
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index 1259eae623de9..133a52e2ce01b 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -391,14 +391,14 @@ export const ExperimentalEditorProvider = withRegistryProvider(
*
* All modification and changes are performed to the `@wordpress/core-data` store.
*
- * @param {Object} props The component props.
- * @param {Object} [props.post] The post object to edit. This is required.
- * @param {Object} [props.__unstableTemplate] The template object wrapper the edited post.
- * This is optional and can only be used when the post type supports templates (like posts and pages).
- * @param {Object} [props.settings] The settings object to use for the editor.
- * This is optional and can be used to override the default settings.
- * @param {Element} [props.children] Children elements for which the BlockEditorProvider context should apply.
- * This is optional.
+ * @param {Object} props The component props.
+ * @param {Object} [props.post] The post object to edit. This is required.
+ * @param {Object} [props.__unstableTemplate] The template object wrapper the edited post.
+ * This is optional and can only be used when the post type supports templates (like posts and pages).
+ * @param {Object} [props.settings] The settings object to use for the editor.
+ * This is optional and can be used to override the default settings.
+ * @param {React.ReactNode} [props.children] Children elements for which the BlockEditorProvider context should apply.
+ * This is optional.
*
* @example
* ```jsx
diff --git a/packages/editor/src/dataviews/fields/content-preview/content-preview-view.tsx b/packages/editor/src/dataviews/fields/content-preview/content-preview-view.tsx
new file mode 100644
index 0000000000000..0a5b838716308
--- /dev/null
+++ b/packages/editor/src/dataviews/fields/content-preview/content-preview-view.tsx
@@ -0,0 +1,108 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ BlockPreview,
+ privateApis as blockEditorPrivateApis,
+ // @ts-ignore
+} from '@wordpress/block-editor';
+import type { BasePost } from '@wordpress/fields';
+import { useSelect } from '@wordpress/data';
+import { useEntityBlockEditor, store as coreStore } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import { EditorProvider } from '../../../components/provider';
+import { unlock } from '../../../lock-unlock';
+// @ts-ignore
+import { store as editorStore } from '../../../store';
+
+const { useGlobalStyle } = unlock( blockEditorPrivateApis );
+
+function PostPreviewContainer( {
+ template,
+ post,
+}: {
+ template: any;
+ post: any;
+} ) {
+ const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' );
+ const [ postBlocks ] = useEntityBlockEditor( 'postType', post.type, {
+ id: post.id,
+ } );
+ const [ templateBlocks ] = useEntityBlockEditor(
+ 'postType',
+ template?.type,
+ {
+ id: template?.id,
+ }
+ );
+ const blocks = template && templateBlocks ? templateBlocks : postBlocks;
+ const isEmpty = ! blocks?.length;
+ return (
+
+ { isEmpty && (
+
+ { __( 'Empty content' ) }
+
+ ) }
+ { ! isEmpty && (
+
+
+
+ ) }
+
+ );
+}
+
+export default function PostPreviewView( { item }: { item: BasePost } ) {
+ const { settings, template } = useSelect(
+ ( select ) => {
+ const { canUser, getPostType, getTemplateId, getEntityRecord } =
+ unlock( select( coreStore ) );
+ const canViewTemplate = canUser( 'read', {
+ kind: 'postType',
+ name: 'wp_template',
+ } );
+ const _settings = select( editorStore ).getEditorSettings();
+ // @ts-ignore
+ const supportsTemplateMode = _settings.supportsTemplateMode;
+ const isViewable = getPostType( item.type )?.viewable ?? false;
+
+ const templateId =
+ supportsTemplateMode && isViewable && canViewTemplate
+ ? getTemplateId( item.type, item.id )
+ : null;
+ return {
+ settings: _settings,
+ template: templateId
+ ? getEntityRecord( 'postType', 'wp_template', templateId )
+ : undefined,
+ };
+ },
+ [ item.type, item.id ]
+ );
+ // Wrap everything in a block editor provider to ensure 'styles' that are needed
+ // for the previews are synced between the site editor store and the block editor store.
+ // Additionally we need to have the `__experimentalBlockPatterns` setting in order to
+ // render patterns inside the previews.
+ // TODO: Same approach is used in the patterns list and it becomes obvious that some of
+ // the block editor settings are needed in context where we don't have the block editor.
+ // Explore how we can solve this in a better way.
+ return (
+
+
+
+ );
+}
diff --git a/packages/editor/src/dataviews/fields/content-preview/index.tsx b/packages/editor/src/dataviews/fields/content-preview/index.tsx
new file mode 100644
index 0000000000000..5dadc599ea232
--- /dev/null
+++ b/packages/editor/src/dataviews/fields/content-preview/index.tsx
@@ -0,0 +1,21 @@
+/**
+ * WordPress dependencies
+ */
+import type { Field } from '@wordpress/dataviews';
+import type { BasePost } from '@wordpress/fields';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import PostPreviewView from './content-preview-view';
+
+const postPreviewField: Field< BasePost > = {
+ type: 'media',
+ id: 'content-preview',
+ label: __( 'Content preview' ),
+ render: PostPreviewView,
+ enableSorting: false,
+};
+
+export default postPreviewField;
diff --git a/packages/editor/src/dataviews/fields/content-preview/style.scss b/packages/editor/src/dataviews/fields/content-preview/style.scss
new file mode 100644
index 0000000000000..4f204dc5108c9
--- /dev/null
+++ b/packages/editor/src/dataviews/fields/content-preview/style.scss
@@ -0,0 +1,21 @@
+.editor-fields-content-preview {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ border-radius: $radius-medium;
+
+ .dataviews-view-table & {
+ width: 96px;
+ flex-grow: 0;
+ }
+
+ .block-editor-block-preview__container,
+ .editor-fields-content-preview__empty {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+}
+
+.editor-fields-content-preview__empty {
+ text-align: center;
+}
diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts
index 2119b52756e96..82c2c8911c7c9 100644
--- a/packages/editor/src/dataviews/store/private-actions.ts
+++ b/packages/editor/src/dataviews/store/private-actions.ts
@@ -38,6 +38,7 @@ import {
* Internal dependencies
*/
import { store as editorStore } from '../../store';
+import postPreviewField from '../fields/content-preview';
import { unlock } from '../../lock-unlock';
export function registerEntityAction< Item >(
@@ -175,6 +176,9 @@ export const registerPostTypeSchema =
postTypeConfig.supports?.comments && commentStatusField,
templateField,
passwordField,
+ postTypeConfig.supports?.editor &&
+ postTypeConfig.viewable &&
+ postPreviewField,
].filter( Boolean );
if ( postTypeConfig.supports?.title ) {
let _titleField;
diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss
index 1a8103ae2b16c..c3366d6aa2266 100644
--- a/packages/editor/src/style.scss
+++ b/packages/editor/src/style.scss
@@ -54,3 +54,4 @@
@import "./components/table-of-contents/style.scss";
@import "./components/text-editor/style.scss";
@import "./components/visual-editor/style.scss";
+@import "./dataviews/fields/content-preview/style.scss";
diff --git a/packages/fields/README.md b/packages/fields/README.md
index 9ca08991aca51..e8224a1e4849a 100644
--- a/packages/fields/README.md
+++ b/packages/fields/README.md
@@ -18,6 +18,10 @@ npm install @wordpress/fields --save
Author field for BasePost.
+### BasePost
+
+Undocumented declaration.
+
### BasePostWithEmbeddedAuthor
Undocumented declaration.
diff --git a/packages/fields/src/fields/featured-image/index.ts b/packages/fields/src/fields/featured-image/index.ts
index d6f22176fc670..7e17fb482e01c 100644
--- a/packages/fields/src/fields/featured-image/index.ts
+++ b/packages/fields/src/fields/featured-image/index.ts
@@ -13,7 +13,7 @@ import { FeaturedImageView } from './featured-image-view';
const featuredImageField: Field< BasePost > = {
id: 'featured_media',
- type: 'text',
+ type: 'media',
label: __( 'Featured Image' ),
Edit: FeaturedImageEdit,
render: FeaturedImageView,
diff --git a/packages/fields/src/index.ts b/packages/fields/src/index.ts
index 1658c9d8c51ee..bf1e4dfda2ddf 100644
--- a/packages/fields/src/index.ts
+++ b/packages/fields/src/index.ts
@@ -1,4 +1,4 @@
export * from './fields';
export * from './actions';
export { default as CreateTemplatePartModal } from './components/create-template-part-modal';
-export type { BasePostWithEmbeddedAuthor, PostType } from './types';
+export type { BasePostWithEmbeddedAuthor, BasePost, PostType } from './types';
diff --git a/packages/fields/src/types.ts b/packages/fields/src/types.ts
index 1b251d125b1be..d9594c58e0979 100644
--- a/packages/fields/src/types.ts
+++ b/packages/fields/src/types.ts
@@ -32,6 +32,9 @@ interface EmbeddedAuthor {
author: Author[];
}
+/**
+ * BasePost interface used for all post types.
+ */
export interface BasePost extends CommonPost {
comment_status?: 'open' | 'closed';
excerpt?: string | { raw: string; rendered: string };
@@ -100,6 +103,7 @@ export interface PostType {
author?: string;
thumbnail?: string;
comments?: string;
+ editor?: boolean;
};
}