diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 836bef5493676..f9a07e2a1830a 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -273,11 +273,13 @@ export default function ViewActions( { } > - + { window?.__experimentalAdminViews && ( + + ) } - ) : ( - - ); + return ; } else if ( path === '/wp_template_part/all' ) { return ; } else if ( path === '/patterns' ) { diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js deleted file mode 100644 index 1121eeb3daa5d..0000000000000 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ /dev/null @@ -1,407 +0,0 @@ -/** - * External dependencies - */ -import removeAccents from 'remove-accents'; - -/** - * WordPress dependencies - */ -import { - Icon, - __experimentalView as View, - __experimentalText as Text, - __experimentalHStack as HStack, - __experimentalVStack as VStack, - VisuallyHidden, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useState, useMemo, useCallback } from '@wordpress/element'; -import { useEntityRecords } from '@wordpress/core-data'; -import { decodeEntities } from '@wordpress/html-entities'; -import { parse } from '@wordpress/blocks'; -import { - BlockPreview, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { DataViews } from '@wordpress/dataviews'; - -/** - * Internal dependencies - */ -import Page from '../page'; -import Link from '../routes/link'; -import { useAddedBy, AvatarImage } from '../list/added-by'; -import { - TEMPLATE_POST_TYPE, - ENUMERATION_TYPE, - OPERATOR_IN, - OPERATOR_NOT_IN, - LAYOUT_GRID, - LAYOUT_TABLE, - LAYOUT_LIST, -} from '../../utils/constants'; -import { - useResetTemplateAction, - deleteTemplateAction, - renameTemplateAction, -} from './template-actions'; -import usePatternSettings from '../page-patterns/use-pattern-settings'; -import { unlock } from '../../lock-unlock'; -import PostPreview from '../post-preview'; - -const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( - blockEditorPrivateApis -); - -const EMPTY_ARRAY = []; - -const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: {}, - [ LAYOUT_GRID ]: { - mediaField: 'preview', - primaryField: 'title', - }, - [ LAYOUT_LIST ]: { - primaryField: 'title', - mediaField: 'preview', - }, -}; - -const DEFAULT_VIEW = { - type: LAYOUT_TABLE, - search: '', - page: 1, - perPage: 20, - // All fields are visible by default, so it's - // better to keep track of the hidden ones. - hiddenFields: [ 'preview' ], - layout: {}, - filters: [], -}; - -function normalizeSearchInput( input = '' ) { - return removeAccents( input.trim().toLowerCase() ); -} - -function TemplateTitle( { item, view } ) { - if ( view.type === LAYOUT_LIST ) { - return ( - <> - { decodeEntities( item.title?.rendered || item.slug ) || - __( '(no title)' ) } - - ); - } - - return ( - - - - { decodeEntities( item.title?.rendered || item.slug ) || - __( '(no title)' ) } - - - - ); -} - -function AuthorField( { item, view } ) { - const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); - const withIcon = view.type !== LAYOUT_LIST; - - return ( - - { withIcon && imageUrl && } - { withIcon && ! imageUrl && ( -
- -
- ) } - { text } -
- ); -} - -function TemplatePreview( { content, viewType } ) { - const settings = usePatternSettings(); - const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); - const blocks = useMemo( () => { - return parse( content ); - }, [ content ] ); - if ( ! blocks?.length ) { - return null; - } - // 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 ( - -
- -
-
- ); -} - -export default function DataviewsTemplates() { - const [ templateId, setTemplateId ] = useState( null ); - const [ view, setView ] = useState( DEFAULT_VIEW ); - const { records: allTemplates, isResolving: isLoadingData } = - useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { - per_page: -1, - } ); - - const onSelectionChange = ( items ) => - setTemplateId( items?.length === 1 ? items[ 0 ].id : null ); - - const authors = useMemo( () => { - if ( ! allTemplates ) { - return EMPTY_ARRAY; - } - const authorsSet = new Set(); - allTemplates.forEach( ( template ) => { - authorsSet.add( template.author_text ); - } ); - return Array.from( authorsSet ).map( ( author ) => ( { - value: author, - label: author, - } ) ); - }, [ allTemplates ] ); - - const fields = useMemo( - () => [ - { - header: __( 'Preview' ), - id: 'preview', - render: ( { item } ) => { - return ( - - ); - }, - minWidth: 120, - maxWidth: 120, - enableSorting: false, - }, - { - header: __( 'Template' ), - id: 'title', - getValue: ( { item } ) => item.title?.rendered || item.slug, - render: ( { item } ) => ( - - ), - maxWidth: 400, - enableHiding: false, - }, - { - header: __( 'Description' ), - id: 'description', - getValue: ( { item } ) => item.description, - render: ( { item } ) => { - return item.description ? ( - decodeEntities( item.description ) - ) : ( - <> - - - { __( 'No description.' ) } - - - ); - }, - maxWidth: 200, - enableSorting: false, - }, - { - header: __( 'Author' ), - id: 'author', - getValue: ( { item } ) => item.author_text, - render: ( { item } ) => { - return ; - }, - enableHiding: false, - type: ENUMERATION_TYPE, - elements: authors, - }, - ], - [ authors, view ] - ); - - const { shownTemplates, paginationInfo } = useMemo( () => { - if ( ! allTemplates ) { - return { - shownTemplates: EMPTY_ARRAY, - paginationInfo: { totalItems: 0, totalPages: 0 }, - }; - } - let filteredTemplates = [ ...allTemplates ]; - // Handle global search. - if ( view.search ) { - const normalizedSearch = normalizeSearchInput( view.search ); - filteredTemplates = filteredTemplates.filter( ( item ) => { - const title = item.title?.rendered || item.slug; - return ( - normalizeSearchInput( title ).includes( - normalizedSearch - ) || - normalizeSearchInput( item.description ).includes( - normalizedSearch - ) - ); - } ); - } - - // Handle filters. - if ( view.filters.length > 0 ) { - view.filters.forEach( ( filter ) => { - if ( - filter.field === 'author' && - filter.operator === OPERATOR_IN && - !! filter.value - ) { - filteredTemplates = filteredTemplates.filter( ( item ) => { - return item.author_text === filter.value; - } ); - } else if ( - filter.field === 'author' && - filter.operator === OPERATOR_NOT_IN && - !! filter.value - ) { - filteredTemplates = filteredTemplates.filter( ( item ) => { - return item.author_text !== filter.value; - } ); - } - } ); - } - - // Handle sorting. - if ( view.sort ) { - const stringSortingFields = [ 'title', 'author' ]; - const fieldId = view.sort.field; - if ( stringSortingFields.includes( fieldId ) ) { - const fieldToSort = fields.find( ( field ) => { - return field.id === fieldId; - } ); - filteredTemplates.sort( ( a, b ) => { - const valueA = fieldToSort.getValue( { item: a } ) ?? ''; - const valueB = fieldToSort.getValue( { item: b } ) ?? ''; - return view.sort.direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); - } ); - } - } - - // Handle pagination. - const start = ( view.page - 1 ) * view.perPage; - const totalItems = filteredTemplates?.length || 0; - filteredTemplates = filteredTemplates?.slice( - start, - start + view.perPage - ); - return { - shownTemplates: filteredTemplates, - paginationInfo: { - totalItems, - totalPages: Math.ceil( totalItems / view.perPage ), - }, - }; - }, [ allTemplates, view, fields ] ); - - const resetTemplateAction = useResetTemplateAction(); - const actions = useMemo( - () => [ - resetTemplateAction, - deleteTemplateAction, - renameTemplateAction, - ], - [ resetTemplateAction ] - ); - const onChangeView = useCallback( - ( viewUpdater ) => { - let updatedView = - typeof viewUpdater === 'function' - ? viewUpdater( view ) - : viewUpdater; - if ( updatedView.type !== view.type ) { - updatedView = { - ...updatedView, - layout: { - ...defaultConfigPerViewType[ updatedView.type ], - }, - }; - } - - setView( updatedView ); - }, - [ view, setView ] - ); - return ( - <> - - item.id } - isLoading={ isLoadingData } - view={ view } - onChangeView={ onChangeView } - onSelectionChange={ onSelectionChange } - deferredRendering={ - ! view.hiddenFields?.includes( 'preview' ) - } - /> - - { view.type === LAYOUT_LIST && ( - -
- { templateId !== null ? ( - - ) : ( -
-

{ __( 'Select a template to preview' ) }

-
- ) } -
-
- ) } - - ); -} diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 55c666970b5cc..f534768a237c4 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -1,92 +1,415 @@ +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; + /** * WordPress dependencies */ import { - VisuallyHidden, - __experimentalHeading as Heading, + Icon, + __experimentalView as View, __experimentalText as Text, + __experimentalHStack as HStack, __experimentalVStack as VStack, + VisuallyHidden, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useState, useMemo, useCallback } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { parse } from '@wordpress/blocks'; +import { + BlockPreview, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { DataViews } from '@wordpress/dataviews'; /** * Internal dependencies */ import Page from '../page'; -import Table from '../table'; import Link from '../routes/link'; -import AddedBy from '../list/added-by'; -import TemplateActions from '../template-actions'; import AddNewTemplate from '../add-new-template'; -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import { useAddedBy, AvatarImage } from '../list/added-by'; +import { + TEMPLATE_POST_TYPE, + ENUMERATION_TYPE, + OPERATOR_IN, + OPERATOR_NOT_IN, + LAYOUT_GRID, + LAYOUT_TABLE, + LAYOUT_LIST, +} from '../../utils/constants'; +import { + useResetTemplateAction, + deleteTemplateAction, + renameTemplateAction, +} from './template-actions'; +import usePatternSettings from '../page-patterns/use-pattern-settings'; +import { unlock } from '../../lock-unlock'; +import PostPreview from '../post-preview'; + +const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( + blockEditorPrivateApis +); + +const EMPTY_ARRAY = []; -export default function PageTemplates() { - const { records: templates } = useEntityRecords( - 'postType', - TEMPLATE_POST_TYPE, - { +const defaultConfigPerViewType = { + [ LAYOUT_TABLE ]: {}, + [ LAYOUT_GRID ]: { + mediaField: 'preview', + primaryField: 'title', + }, + [ LAYOUT_LIST ]: { + primaryField: 'title', + mediaField: 'preview', + }, +}; + +const DEFAULT_VIEW = { + type: LAYOUT_TABLE, + search: '', + page: 1, + perPage: 20, + // All fields are visible by default, so it's + // better to keep track of the hidden ones. + hiddenFields: [ 'preview' ], + layout: {}, + filters: [], +}; + +function normalizeSearchInput( input = '' ) { + return removeAccents( input.trim().toLowerCase() ); +} + +function TemplateTitle( { item, view } ) { + if ( view.type === LAYOUT_LIST ) { + return ( + <> + { decodeEntities( item.title?.rendered || item.slug ) || + __( '(no title)' ) } + + ); + } + + return ( + + + + { decodeEntities( item.title?.rendered || item.slug ) || + __( '(no title)' ) } + + + + ); +} + +function AuthorField( { item, view } ) { + const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + const withIcon = view.type !== LAYOUT_LIST; + + return ( + + { withIcon && imageUrl && } + { withIcon && ! imageUrl && ( +
+ +
+ ) } + { text } +
+ ); +} + +function TemplatePreview( { content, viewType } ) { + const settings = usePatternSettings(); + const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); + const blocks = useMemo( () => { + return parse( content ); + }, [ content ] ); + if ( ! blocks?.length ) { + return null; + } + // 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 ( + +
+ +
+
+ ); +} + +export default function DataviewsTemplates() { + const [ templateId, setTemplateId ] = useState( null ); + const [ view, setView ] = useState( DEFAULT_VIEW ); + const { records: allTemplates, isResolving: isLoadingData } = + useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, + } ); + + const onSelectionChange = ( items ) => + setTemplateId( items?.length === 1 ? items[ 0 ].id : null ); + + const authors = useMemo( () => { + if ( ! allTemplates ) { + return EMPTY_ARRAY; } + const authorsSet = new Set(); + allTemplates.forEach( ( template ) => { + authorsSet.add( template.author_text ); + } ); + return Array.from( authorsSet ).map( ( author ) => ( { + value: author, + label: author, + } ) ); + }, [ allTemplates ] ); + + const fields = useMemo( + () => [ + { + header: __( 'Preview' ), + id: 'preview', + render: ( { item } ) => { + return ( + + ); + }, + minWidth: 120, + maxWidth: 120, + enableSorting: false, + }, + { + header: __( 'Template' ), + id: 'title', + getValue: ( { item } ) => item.title?.rendered || item.slug, + render: ( { item } ) => ( + + ), + maxWidth: 400, + enableHiding: false, + }, + { + header: __( 'Description' ), + id: 'description', + getValue: ( { item } ) => item.description, + render: ( { item } ) => { + return item.description ? ( + decodeEntities( item.description ) + ) : ( + <> + + + { __( 'No description.' ) } + + + ); + }, + maxWidth: 200, + enableSorting: false, + }, + { + header: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item.author_text, + render: ( { item } ) => { + return ; + }, + enableHiding: false, + type: ENUMERATION_TYPE, + elements: authors, + }, + ], + [ authors, view ] ); - const columns = [ - { - header: __( 'Template' ), - cell: ( template ) => ( - - - - { decodeEntities( - template.title?.rendered || template.slug - ) } - - - { template.description && ( - - { decodeEntities( template.description ) } - - ) } - - ), - maxWidth: 400, - }, - { - header: __( 'Added by' ), - cell: ( template ) => ( - - ), - }, - { - header: { __( 'Actions' ) }, - cell: ( template ) => ( - - ), - }, - ]; + const { shownTemplates, paginationInfo } = useMemo( () => { + if ( ! allTemplates ) { + return { + shownTemplates: EMPTY_ARRAY, + paginationInfo: { totalItems: 0, totalPages: 0 }, + }; + } + let filteredTemplates = [ ...allTemplates ]; + // Handle global search. + if ( view.search ) { + const normalizedSearch = normalizeSearchInput( view.search ); + filteredTemplates = filteredTemplates.filter( ( item ) => { + const title = item.title?.rendered || item.slug; + return ( + normalizeSearchInput( title ).includes( + normalizedSearch + ) || + normalizeSearchInput( item.description ).includes( + normalizedSearch + ) + ); + } ); + } + // Handle filters. + if ( view.filters.length > 0 ) { + view.filters.forEach( ( filter ) => { + if ( + filter.field === 'author' && + filter.operator === OPERATOR_IN && + !! filter.value + ) { + filteredTemplates = filteredTemplates.filter( ( item ) => { + return item.author_text === filter.value; + } ); + } else if ( + filter.field === 'author' && + filter.operator === OPERATOR_NOT_IN && + !! filter.value + ) { + filteredTemplates = filteredTemplates.filter( ( item ) => { + return item.author_text !== filter.value; + } ); + } + } ); + } + + // Handle sorting. + if ( view.sort ) { + const stringSortingFields = [ 'title', 'author' ]; + const fieldId = view.sort.field; + if ( stringSortingFields.includes( fieldId ) ) { + const fieldToSort = fields.find( ( field ) => { + return field.id === fieldId; + } ); + filteredTemplates.sort( ( a, b ) => { + const valueA = fieldToSort.getValue( { item: a } ) ?? ''; + const valueB = fieldToSort.getValue( { item: b } ) ?? ''; + return view.sort.direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + } ); + } + } + + // Handle pagination. + const start = ( view.page - 1 ) * view.perPage; + const totalItems = filteredTemplates?.length || 0; + filteredTemplates = filteredTemplates?.slice( + start, + start + view.perPage + ); + return { + shownTemplates: filteredTemplates, + paginationInfo: { + totalItems, + totalPages: Math.ceil( totalItems / view.perPage ), + }, + }; + }, [ allTemplates, view, fields ] ); + + const resetTemplateAction = useResetTemplateAction(); + const actions = useMemo( + () => [ + resetTemplateAction, + deleteTemplateAction, + renameTemplateAction, + ], + [ resetTemplateAction ] + ); + const onChangeView = useCallback( + ( viewUpdater ) => { + let updatedView = + typeof viewUpdater === 'function' + ? viewUpdater( view ) + : viewUpdater; + if ( updatedView.type !== view.type ) { + updatedView = { + ...updatedView, + layout: { + ...defaultConfigPerViewType[ updatedView.type ], + }, + }; + } + + setView( updatedView ); + }, + [ view, setView ] + ); return ( - + + } + > + item.id } + isLoading={ isLoadingData } + view={ view } + onChangeView={ onChangeView } + onSelectionChange={ onSelectionChange } + deferredRendering={ + ! view.hiddenFields?.includes( 'preview' ) + } /> - } - > - { templates && } - + + { view.type === LAYOUT_LIST && ( + +
+ { templateId !== null ? ( + + ) : ( +
+

{ __( 'Select a template to preview' ) }

+
+ ) } +
+
+ ) } + ); }