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 ( +
+
+ { + onChangeControl( selectedMedia.id ); + } } + allowedTypes={ [ 'image' ] } + render={ ( { open }: { open: () => void } ) => { + return ( +
{ + open(); + } } + onKeyDown={ open } + > + + { url && ( + <> + + + { title } + + + ) } + { ! url && ( + <> + + + { __( 'Choose an imageā€¦' ) } + + + ) } + { url && ( + <> +
+ ); + } } + /> +
+
+ ); +}; 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" ] }