From 859d48b64dfc10c997f093ebea1a25d557c2f9c4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 22 Jul 2024 18:22:53 +0200 Subject: [PATCH] DataViews: Bootstrap Quick Edit (#63600) Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: oandregal Co-authored-by: jameskoster --- lib/experimental/editor-settings.php | 3 + lib/experiments-page.php | 12 + .../src/components/dataviews/index.tsx | 18 +- .../dataviews/src/layouts/table/index.tsx | 2 +- .../edit-site/src/components/layout/index.js | 11 + .../edit-site/src/components/layout/router.js | 12 +- .../src/components/post-edit/index.js | 86 ++++ .../src/components/post-edit/style.scss | 9 + .../src/components/post-fields/index.js | 345 ++++++++++++++++ .../src/components/post-list/index.js | 367 ++---------------- packages/edit-site/src/style.scss | 1 + 11 files changed, 531 insertions(+), 335 deletions(-) create mode 100644 packages/edit-site/src/components/post-edit/index.js create mode 100644 packages/edit-site/src/components/post-edit/style.scss create mode 100644 packages/edit-site/src/components/post-fields/index.js diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 2af7cafb4838ca..09ac4b6990d919 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -31,6 +31,9 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-zoomed-out-patterns-tab', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableZoomedOutPatternsTab = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-quick-edit-dataviews', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalQuickEditDataViews = true', 'before' ); + } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 74a133da05c976..7cc4198c14ef91 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -151,6 +151,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-quick-edit-dataviews', + __( 'Quick Edit in DataViews', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Allow access to a quick edit panel in the pages data views.', 'gutenberg' ), + 'id' => 'gutenberg-quick-edit-dataviews', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index 5d45413f03b65a..c2f75b9b32b1e8 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; + /** * WordPress dependencies */ @@ -39,6 +44,7 @@ type DataViewsProps< Item > = { defaultLayouts: SupportedLayouts; selection?: string[]; onChangeSelection?: ( items: string[] ) => void; + header?: ReactNode; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); @@ -59,6 +65,7 @@ export default function DataViews< Item >( { defaultLayouts, selection: selectionProperty, onChangeSelection, + header, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); const [ density, setDensity ] = useState< number >( 0 ); @@ -122,7 +129,16 @@ export default function DataViews< Item >( { /> ) } - + + + { header } + diff --git a/packages/dataviews/src/layouts/table/index.tsx b/packages/dataviews/src/layouts/table/index.tsx index 934cff9e23affa..df66f225988a7d 100644 --- a/packages/dataviews/src/layouts/table/index.tsx +++ b/packages/dataviews/src/layouts/table/index.tsx @@ -238,7 +238,7 @@ function TableRow< Item >( { onChangeSelection( selection.includes( id ) ? selection.filter( ( itemId ) => id !== itemId ) - : [ ...selection, id ] + : [ id ] ); } } } diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index a609f616471dbb..a4e4fa57a5bbf1 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -201,6 +201,17 @@ export default function Layout( { route } ) { ) } + { ! isMobileViewport && areas.edit && ( +
+ { areas.edit } +
+ ) } + { ! isMobileViewport && areas.preview && (
{ canvasResizer } diff --git a/packages/edit-site/src/components/layout/router.js b/packages/edit-site/src/components/layout/router.js index b89c1a5f256937..3fd0cc560d9433 100644 --- a/packages/edit-site/src/components/layout/router.js +++ b/packages/edit-site/src/components/layout/router.js @@ -26,6 +26,7 @@ import { TEMPLATE_PART_POST_TYPE, TEMPLATE_POST_TYPE, } from '../../utils/constants'; +import { PostEdit } from '../post-edit'; const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -74,13 +75,15 @@ function useRedirectOldPaths() { export default function useLayoutAreas() { const { params } = useLocation(); - const { postType, postId, path, layout, isCustom, canvas } = params; + const { postType, postId, path, layout, isCustom, canvas, quickEdit } = + params; const hasEditCanvasMode = canvas === 'edit'; useRedirectOldPaths(); // Page list if ( postType === 'page' ) { const isListLayout = layout === 'list' || ! layout; + const showQuickEdit = quickEdit && ! isListLayout; return { key: 'pages', areas: { @@ -92,15 +95,20 @@ export default function useLayoutAreas() { /> ), content: , - preview: ( isListLayout || hasEditCanvasMode ) && , + preview: ! showQuickEdit && + ( isListLayout || hasEditCanvasMode ) && , mobile: hasEditCanvasMode ? ( ) : ( ), + edit: showQuickEdit && ( + + ), }, widths: { content: isListLayout ? 380 : undefined, + edit: showQuickEdit ? 380 : undefined, }, }; } diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js new file mode 100644 index 00000000000000..6e556c56a9152c --- /dev/null +++ b/packages/edit-site/src/components/post-edit/index.js @@ -0,0 +1,86 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { DataForm } from '@wordpress/dataviews'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; +import { Button } from '@wordpress/components'; +import { useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Page from '../page'; +import usePostFields from '../post-fields'; + +function PostEditForm( { postType, postId } ) { + const { item } = useSelect( + ( select ) => { + return { + item: select( coreDataStore ).getEntityRecord( + 'postType', + postType, + postId + ), + }; + }, + [ postType, postId ] + ); + const { saveEntityRecord } = useDispatch( coreDataStore ); + const { fields } = usePostFields(); + const form = { + visibleFields: [ 'title' ], + }; + const [ edits, setEdits ] = useState( {} ); + const itemWithEdits = useMemo( () => { + return { + ...item, + ...edits, + }; + }, [ item, edits ] ); + const onSubmit = ( event ) => { + event.preventDefault(); + saveEntityRecord( 'postType', postType, itemWithEdits ); + setEdits( {} ); + }; + + if ( ! item ) { + return null; + } + + return ( +
+ + + + ); +} + +export function PostEdit( { postType, postId } ) { + return ( + + { postId && ( + + ) } + { ! postId &&

{ __( 'Select a page to edit' ) }

} +
+ ); +} diff --git a/packages/edit-site/src/components/post-edit/style.scss b/packages/edit-site/src/components/post-edit/style.scss new file mode 100644 index 00000000000000..4eaa41b2e8ed70 --- /dev/null +++ b/packages/edit-site/src/components/post-edit/style.scss @@ -0,0 +1,9 @@ +.edit-site-post-edit { + padding: $grid-unit-30; + + &.is-empty .edit-site-page-content { + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js new file mode 100644 index 00000000000000..5a94b60f5dde0f --- /dev/null +++ b/packages/edit-site/src/components/post-fields/index.js @@ -0,0 +1,345 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; +import { + createInterpolateElement, + useMemo, + useState, +} from '@wordpress/element'; +import { dateI18n, getDate, getSettings } from '@wordpress/date'; +import { + trash, + drafts, + published, + scheduled, + pending, + notAllowed, + commentAuthorAvatar as authorIcon, +} from '@wordpress/icons'; +import { __experimentalHStack as HStack, Icon } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +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'; + +// See https://github.com/WordPress/gutenberg/issues/55886 +// We do not support custom statutes at the moment. +const STATUSES = [ + { value: 'draft', label: __( 'Draft' ), icon: drafts }, + { value: 'future', label: __( 'Scheduled' ), icon: scheduled }, + { value: 'pending', label: __( 'Pending Review' ), icon: pending }, + { value: 'private', label: __( 'Private' ), icon: notAllowed }, + { value: 'publish', label: __( 'Published' ), icon: published }, + { value: 'trash', label: __( 'Trash' ), icon: trash }, +]; + +const getFormattedDate = ( dateToDisplay ) => + dateI18n( + getSettings().formats.datetimeAbbreviated, + 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; + const icon = status?.icon; + return ( + + { icon && ( +
+ +
+ ) } + { label } +
+ ); +} + +function PostAuthorField( { item } ) { + const { text, imageUrl } = useSelect( + ( select ) => { + const { getUser } = select( coreStore ); + const user = getUser( item.author ); + return { + imageUrl: user?.avatar_urls?.[ 48 ], + text: user?.name, + }; + }, + [ item ] + ); + const [ isImageLoaded, setIsImageLoaded ] = useState( false ); + return ( + + { !! imageUrl && ( +
+ setIsImageLoaded( true ) } + alt={ __( 'Author avatar' ) } + src={ imageUrl } + /> +
+ ) } + { ! imageUrl && ( +
+ +
+ ) } + { text } +
+ ); +} + +function usePostFields( viewType ) { + const { records: authors, isResolving: isLoadingAuthors } = + useEntityRecords( 'root', 'user', { per_page: -1 } ); + + const { frontPageId, postsPageId } = useSelect( ( select ) => { + const { getEntityRecord } = select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + return { + frontPageId: siteSettings?.page_on_front, + postsPageId: siteSettings?.page_for_posts, + }; + }, [] ); + + const fields = useMemo( + () => [ + { + id: 'featured-image', + header: __( 'Featured Image' ), + getValue: ( { item } ) => item.featured_media, + render: ( { item } ) => ( + + ), + enableSorting: false, + }, + { + header: __( 'Title' ), + id: 'title', + type: 'text', + getValue: ( { item } ) => + typeof item.title === 'string' + ? item.title + : item.title?.raw, + render: ( { item } ) => { + const addLink = + [ LAYOUT_TABLE, LAYOUT_GRID ].includes( viewType ) && + item.status !== 'trash'; + const title = addLink ? ( + + { decodeEntities( item.title?.rendered ) || + __( '(no title)' ) } + + ) : ( + + { decodeEntities( item.title?.rendered ) || + __( '(no title)' ) } + + ); + + let suffix = ''; + if ( item.id === frontPageId ) { + suffix = ( + + { __( 'Homepage' ) } + + ); + } else if ( item.id === postsPageId ) { + suffix = ( + + { __( 'Posts Page' ) } + + ); + } + + return ( + + { title } + { suffix } + + ); + }, + enableHiding: false, + }, + { + header: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item._embedded?.author[ 0 ]?.name, + elements: + authors?.map( ( { id, name } ) => ( { + value: id, + label: name, + } ) ) || [], + render: PostAuthorField, + }, + { + header: __( 'Status' ), + id: 'status', + getValue: ( { item } ) => + STATUSES.find( ( { value } ) => value === item.status ) + ?.label ?? item.status, + elements: STATUSES, + render: PostStatusField, + enableSorting: false, + filterBy: { + operators: [ OPERATOR_IS_ANY ], + }, + }, + { + header: __( 'Date' ), + id: 'date', + render: ( { item } ) => { + const isDraftOrPrivate = [ 'draft', 'private' ].includes( + item.status + ); + if ( isDraftOrPrivate ) { + return createInterpolateElement( + sprintf( + /* translators: %s: page creation date */ + __( 'Modified: ' ), + getFormattedDate( item.date ) + ), + { + span: , + time: