diff --git a/package-lock.json b/package-lock.json
index 76bc76d72f33a2..883efd7e4ed703 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54065,6 +54065,7 @@
"@wordpress/editor": "*",
"@wordpress/element": "*",
"@wordpress/escape-html": "*",
+ "@wordpress/fields": "*",
"@wordpress/hooks": "*",
"@wordpress/html-entities": "*",
"@wordpress/i18n": "*",
@@ -54457,10 +54458,12 @@
"@wordpress/html-entities": "*",
"@wordpress/i18n": "*",
"@wordpress/icons": "*",
+ "@wordpress/media-utils": "*",
"@wordpress/notices": "*",
"@wordpress/patterns": "*",
"@wordpress/primitives": "*",
"@wordpress/private-apis": "*",
+ "@wordpress/router": "*",
"@wordpress/url": "*",
"@wordpress/warning": "*",
"change-case": "4.1.2",
diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss
index 77238c6f386084..7bb71732546053 100644
--- a/packages/base-styles/_z-index.scss
+++ b/packages/base-styles/_z-index.scss
@@ -206,6 +206,9 @@ $z-layers: (
// Ensure footer stays above the preview field.
".dataviews-footer": 2,
+
+ // Needs to be below media library (.media-model) that has a z-index of 160000.
+ ".components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown": 160000 - 10,
);
@function z-index( $key ) {
diff --git a/packages/core-data/src/entity-types/attachment.ts b/packages/core-data/src/entity-types/attachment.ts
index 703a08611c0e2b..3d4e6814e29cf6 100644
--- a/packages/core-data/src/entity-types/attachment.ts
+++ b/packages/core-data/src/entity-types/attachment.ts
@@ -14,6 +14,40 @@ import type {
import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records';
+interface MediaDetails {
+ width: number;
+ height: number;
+ file: string;
+ filesize: number;
+ sizes: { [ key: string ]: Size };
+ image_meta: ImageMeta;
+ original_image?: string;
+}
+interface ImageMeta {
+ aperture: string;
+ credit: string;
+ camera: string;
+ caption: string;
+ created_timestamp: string;
+ copyright: string;
+ focal_length: string;
+ iso: string;
+ shutter_speed: string;
+ title: string;
+ orientation: string;
+ keywords: any[];
+}
+
+interface Size {
+ file: string;
+ width: number;
+ height: number;
+ filesize?: number;
+ mime_type: string;
+ source_url: string;
+ uncropped?: boolean;
+}
+
declare module './base-entity-records' {
export namespace BaseEntityRecords {
export interface Attachment< C extends Context > {
@@ -124,7 +158,7 @@ declare module './base-entity-records' {
/**
* Details about the media file, specific to its type.
*/
- media_details: Record< string, string >;
+ media_details: MediaDetails;
/**
* The ID for the associated post of the attachment.
*/
diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts
index bc44b57eaaecc6..2a335dce3af32b 100644
--- a/packages/dataviews/src/types.ts
+++ b/packages/dataviews/src/types.ts
@@ -106,7 +106,7 @@ export type Field< Item > = {
/**
* Callback used to render the field. Defaults to `field.getValue`.
*/
- render?: ComponentType< { item: Item } >;
+ render?: ComponentType< DataViewRenderFieldProps< Item > >;
/**
* Callback used to render an edit control for the field.
@@ -159,7 +159,7 @@ export type NormalizedField< Item > = Field< Item > & {
label: string;
header: string | ReactElement;
getValue: ( args: { item: Item } ) => any;
- render: ComponentType< { item: Item } >;
+ render: ComponentType< DataViewRenderFieldProps< Item > >;
Edit: ComponentType< DataFormControlProps< Item > >;
sort: ( a: Item, b: Item, direction: SortDirection ) => number;
isValid: ( item: Item, context?: ValidationContext ) => boolean;
@@ -181,6 +181,10 @@ export type DataFormControlProps< Item > = {
hideLabelFromVision?: boolean;
};
+export type DataViewRenderFieldProps< Item > = {
+ item: Item;
+};
+
/**
* The filters applied to the dataset.
*/
diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json
index 83cae4a7100bc7..f85a9e26a9d689 100644
--- a/packages/edit-site/package.json
+++ b/packages/edit-site/package.json
@@ -48,6 +48,7 @@
"@wordpress/editor": "*",
"@wordpress/element": "*",
"@wordpress/escape-html": "*",
+ "@wordpress/fields": "*",
"@wordpress/hooks": "*",
"@wordpress/html-entities": "*",
"@wordpress/i18n": "*",
diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js
index 8d7e09c702047e..a1523142fb6691 100644
--- a/packages/edit-site/src/components/post-edit/index.js
+++ b/packages/edit-site/src/components/post-edit/index.js
@@ -60,14 +60,20 @@ function PostEditForm( { postType, postId } ) {
);
const form = {
type: 'panel',
- fields: [ 'title', 'status', 'date', 'author', 'comment_status' ],
+ fields: [
+ 'featured_media',
+ 'title',
+ 'author',
+ 'date',
+ 'comment_status',
+ ],
};
const onChange = ( edits ) => {
for ( const id of ids ) {
if (
edits.status &&
edits.status !== 'future' &&
- record.status === 'future' &&
+ record?.status === 'future' &&
new Date( record.date ) > new Date()
) {
edits.date = null;
diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js
index 9e59b23d61922d..c40b86c8026536 100644
--- a/packages/edit-site/src/components/post-fields/index.js
+++ b/packages/edit-site/src/components/post-fields/index.js
@@ -8,6 +8,7 @@ import clsx from 'clsx';
*/
import { __, sprintf } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
+import { featuredImageField } from '@wordpress/fields';
import {
createInterpolateElement,
useMemo,
@@ -33,11 +34,9 @@ import { useEntityRecords, store as coreStore } from '@wordpress/core-data';
import {
LAYOUT_GRID,
LAYOUT_TABLE,
- LAYOUT_LIST,
OPERATOR_IS_ANY,
} from '../../utils/constants';
-import { default as Link, useLink } from '../routes/link';
-import Media from '../media';
+import { default as Link } from '../routes/link';
// See https://github.com/WordPress/gutenberg/issues/55886
// We do not support custom statutes at the moment.
@@ -81,46 +80,6 @@ const getFormattedDate = ( dateToDisplay ) =>
getDate( dateToDisplay )
);
-function FeaturedImage( { item, viewType } ) {
- const isDisabled = item.status === 'trash';
- const { onClick } = useLink( {
- postId: item.id,
- postType: item.type,
- canvas: 'edit',
- } );
- const hasMedia = !! item.featured_media;
- const size =
- viewType === LAYOUT_GRID
- ? [ 'large', 'full', 'medium', 'thumbnail' ]
- : [ 'thumbnail', 'medium', 'large', 'full' ];
- const media = hasMedia ? (
-
- ) : null;
- const renderButton = viewType !== LAYOUT_LIST && ! isDisabled;
- return (
-
- { renderButton ? (
-
- ) : (
- media
- ) }
-
- );
-}
-
function PostStatusField( { item } ) {
const status = STATUSES.find( ( { value } ) => value === item.status );
const label = status?.label || item.status;
@@ -190,15 +149,7 @@ function usePostFields( viewType ) {
const fields = useMemo(
() => [
- {
- id: 'featured-image',
- label: __( 'Featured Image' ),
- getValue: ( { item } ) => item.featured_media,
- render: ( { item } ) => (
-
- ),
- enableSorting: false,
- },
+ featuredImageField,
{
label: __( 'Title' ),
id: 'title',
diff --git a/packages/edit-site/src/components/post-fields/style.scss b/packages/edit-site/src/components/post-fields/style.scss
new file mode 100644
index 00000000000000..adeaf9a2678253
--- /dev/null
+++ b/packages/edit-site/src/components/post-fields/style.scss
@@ -0,0 +1,3 @@
+.components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown {
+ z-index: z-index(".components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown");
+}
diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js
index 658fa319e9c667..72f4c94fe6bcdd 100644
--- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js
+++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js
@@ -31,9 +31,6 @@ export const defaultLayouts = {
layout: {
primaryField: 'title',
styles: {
- 'featured-image': {
- width: '1%',
- },
title: {
maxWidth: 300,
},
@@ -42,14 +39,14 @@ export const defaultLayouts = {
},
[ LAYOUT_GRID ]: {
layout: {
- mediaField: 'featured-image',
+ mediaField: 'featured_media',
primaryField: 'title',
},
},
[ LAYOUT_LIST ]: {
layout: {
primaryField: 'title',
- mediaField: 'featured-image',
+ mediaField: 'featured_media',
},
},
};
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index 4e9e1071e3a77a..1668a940fb28fa 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -1,4 +1,5 @@
@import "../../dataviews/src/style.scss";
+@import "../../fields/src/fields/featured-image/style.scss";
@import "./components/add-new-template/style.scss";
@import "./components/block-editor/style.scss";
@@ -30,6 +31,7 @@
@import "./components/editor-canvas-container/style.scss";
@import "./components/post-edit/style.scss";
@import "./components/post-list/style.scss";
+@import "./components/post-fields/style.scss";
@import "./components/resizable-frame/style.scss";
@import "./hooks/push-changes-to-global-styles/style.scss";
@import "./components/global-styles/font-library-modal/style.scss";
diff --git a/packages/fields/README.md b/packages/fields/README.md
index b4e45103600da6..fdcedac3032dbd 100644
--- a/packages/fields/README.md
+++ b/packages/fields/README.md
@@ -38,6 +38,10 @@ Undocumented declaration.
Undocumented declaration.
+### featuredImageField
+
+Undocumented declaration.
+
### orderField
Undocumented declaration.
diff --git a/packages/fields/package.json b/packages/fields/package.json
index 019ec99ed7a8ed..43772cb41981a5 100644
--- a/packages/fields/package.json
+++ b/packages/fields/package.json
@@ -45,10 +45,12 @@
"@wordpress/html-entities": "*",
"@wordpress/i18n": "*",
"@wordpress/icons": "*",
+ "@wordpress/media-utils": "*",
"@wordpress/notices": "*",
"@wordpress/patterns": "*",
"@wordpress/primitives": "*",
"@wordpress/private-apis": "*",
+ "@wordpress/router": "*",
"@wordpress/url": "*",
"@wordpress/warning": "*",
"change-case": "4.1.2",
diff --git a/packages/fields/src/fields/featured-image/featured-image-edit.tsx b/packages/fields/src/fields/featured-image/featured-image-edit.tsx
new file mode 100644
index 00000000000000..b0dc612cdcfa50
--- /dev/null
+++ b/packages/fields/src/fields/featured-image/featured-image-edit.tsx
@@ -0,0 +1,122 @@
+/**
+ * WordPress dependencies
+ */
+import { Button, __experimentalGrid as Grid } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { useCallback, useRef } from '@wordpress/element';
+// @ts-ignore
+import { MediaUpload } from '@wordpress/media-utils';
+import { lineSolid } from '@wordpress/icons';
+import { store as coreStore } from '@wordpress/core-data';
+import type { DataFormControlProps } from '@wordpress/dataviews';
+/**
+ * Internal dependencies
+ */
+import type { BasePost } from '../../types';
+import { __ } from '@wordpress/i18n';
+
+export const FeaturedImageEdit = ( {
+ data,
+ field,
+ onChange,
+}: DataFormControlProps< BasePost > ) => {
+ const { id } = field;
+
+ const value = field.getValue( { item: data } );
+
+ const media = useSelect(
+ ( select ) => {
+ const { getEntityRecord } = select( coreStore );
+ return getEntityRecord( 'root', 'media', value );
+ },
+ [ value ]
+ );
+
+ const onChangeControl = useCallback(
+ ( newValue: number ) =>
+ onChange( {
+ [ id ]: newValue,
+ } ),
+ [ id, onChange ]
+ );
+
+ const url = media?.source_url;
+ const title = media?.title?.rendered;
+ const ref = useRef( null );
+
+ return (
+
+ );
+};
diff --git a/packages/fields/src/fields/featured-image/featured-image-view.tsx b/packages/fields/src/fields/featured-image/featured-image-view.tsx
new file mode 100644
index 00000000000000..36793e6f2ff9ab
--- /dev/null
+++ b/packages/fields/src/fields/featured-image/featured-image-view.tsx
@@ -0,0 +1,38 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import type { BasePost } from '../../types';
+import type { DataViewRenderFieldProps } from '@wordpress/dataviews';
+
+export const FeaturedImageView = ( {
+ item,
+}: DataViewRenderFieldProps< BasePost > ) => {
+ const mediaId = item.featured_media;
+
+ const media = useSelect(
+ ( select ) => {
+ const { getEntityRecord } = select( coreStore );
+ return mediaId ? getEntityRecord( 'root', 'media', mediaId ) : null;
+ },
+ [ mediaId ]
+ );
+ const url = media?.source_url;
+
+ if ( url ) {
+ return (
+
+ );
+ }
+
+ return ;
+};
diff --git a/packages/fields/src/fields/featured-image/index.ts b/packages/fields/src/fields/featured-image/index.ts
new file mode 100644
index 00000000000000..44f9a1b4064648
--- /dev/null
+++ b/packages/fields/src/fields/featured-image/index.ts
@@ -0,0 +1,24 @@
+/**
+ * WordPress dependencies
+ */
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { BasePost } from '../../types';
+import { __ } from '@wordpress/i18n';
+import { FeaturedImageEdit } from './featured-image-edit';
+import { FeaturedImageView } from './featured-image-view';
+
+const featuredImageField: Field< BasePost > = {
+ id: 'featured_media',
+ type: 'text',
+ label: __( 'Featured Image' ),
+ getValue: ( { item } ) => item.featured_media,
+ Edit: FeaturedImageEdit,
+ render: FeaturedImageView,
+ enableSorting: false,
+};
+
+export default featuredImageField;
diff --git a/packages/fields/src/fields/featured-image/style.scss b/packages/fields/src/fields/featured-image/style.scss
new file mode 100644
index 00000000000000..46d37960199ead
--- /dev/null
+++ b/packages/fields/src/fields/featured-image/style.scss
@@ -0,0 +1,95 @@
+.fields-controls__featured-image-placeholder {
+ border-radius: $radius-small;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
+ display: inline-block;
+ padding: 0;
+ background:
+ $white
+ linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%);
+}
+
+.fields-controls__featured-image-title {
+ width: 100%;
+ color: $gray-900;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.fields-controls__featured-image-image {
+ width: 100%;
+ height: 100%;
+ border-radius: $radius-small;
+ align-self: center;
+}
+
+.fields-controls__featured-image-container {
+ .fields-controls__featured-image-placeholder {
+ margin: 0;
+ }
+
+ span {
+ margin-right: auto;
+ }
+}
+
+fieldset.fields-controls__featured-image {
+ .fields-controls__featured-image-container {
+ border: $border-width solid $gray-300;
+ border-radius: $radius-small;
+ padding: 8px 12px;
+ cursor: pointer;
+ &:hover {
+ background-color: $gray-100;
+ }
+ }
+
+ .fields-controls__featured-image-placeholder {
+ width: 24px;
+ height: 24px;
+ }
+
+ span {
+ align-self: center;
+ text-align: start;
+ white-space: nowrap;
+ }
+
+ .fields-controls__featured-image-upload-button {
+ padding: 0;
+ height: fit-content;
+ &:hover,
+ &:focus {
+ border: 0;
+ color: unset;
+ }
+ }
+
+ .fields-controls__featured-image-remove-button {
+ place-self: end;
+ }
+}
+
+.dataforms-layouts-panel__field-control {
+ .fields-controls__featured-image-image {
+ width: 16px;
+ height: 16px;
+ }
+
+ .fields-controls__featured-image-placeholder {
+ width: 16px;
+ height: 16px;
+ }
+}
+
+.dataviews-view-table__cell-content-wrapper {
+ .fields-controls__featured-image-image {
+ width: 32px;
+ height: 32px;
+ }
+
+ .fields-controls__featured-image-placeholder {
+ width: 32px;
+ height: 32px;
+ }
+}
diff --git a/packages/fields/src/fields/index.ts b/packages/fields/src/fields/index.ts
index 63ff87842fa4c9..62a1e1485e75e8 100644
--- a/packages/fields/src/fields/index.ts
+++ b/packages/fields/src/fields/index.ts
@@ -1,2 +1,3 @@
export { default as titleField } from './title';
export { default as orderField } from './order';
+export { default as featuredImageField } from './featured-image';
diff --git a/packages/fields/tsconfig.json b/packages/fields/tsconfig.json
index 69dbd076d05747..c553ee023993d0 100644
--- a/packages/fields/tsconfig.json
+++ b/packages/fields/tsconfig.json
@@ -23,7 +23,9 @@
{ "path": "../blob" },
{ "path": "../core-data" },
{ "path": "../hooks" },
- { "path": "../html-entities" }
+ { "path": "../html-entities" },
+ { "path": "../media-utils" },
+ { "path": "../router" }
],
"include": [ "src" ]
}