diff --git a/packages/dataviews/src/layouts.ts b/packages/dataviews/src/layouts.ts index 0d00263b6fbc55..f8339c0a6b83f7 100644 --- a/packages/dataviews/src/layouts.ts +++ b/packages/dataviews/src/layouts.ts @@ -16,6 +16,7 @@ import ViewTable from './view-table'; import ViewGrid from './view-grid'; import ViewList from './view-list'; import { LAYOUT_GRID, LAYOUT_LIST, LAYOUT_TABLE } from './constants'; +import type { View } from './types'; export const VIEW_LAYOUTS = [ { @@ -37,3 +38,29 @@ export const VIEW_LAYOUTS = [ icon: isRTL() ? formatListBulletsRTL : formatListBullets, }, ]; + +export function getMandatoryFields( view: View ): string[] { + if ( view.type === 'table' ) { + return [ view.layout?.primaryField ] + .concat( + view.layout?.combinedFields?.flatMap( + ( field ) => field.children + ) ?? [] + ) + .filter( ( item ): item is string => !! item ); + } + + if ( view.type === 'grid' ) { + return [ view.layout?.primaryField, view.layout?.mediaField ].filter( + ( item ): item is string => !! item + ); + } + + if ( view.type === 'list' ) { + return [ view.layout?.primaryField, view.layout?.mediaField ].filter( + ( item ): item is string => !! item + ); + } + + return []; +} diff --git a/packages/dataviews/src/stories/fixtures.js b/packages/dataviews/src/stories/fixtures.js index 133f8d3fea573c..cb107e56969d17 100644 --- a/packages/dataviews/src/stories/fixtures.js +++ b/packages/dataviews/src/stories/fixtures.js @@ -167,20 +167,17 @@ export const fields = [ ); }, - width: 50, enableSorting: false, }, { header: 'Title', id: 'title', - maxWidth: 400, enableHiding: false, enableGlobalSearch: true, }, { header: 'Type', id: 'type', - maxWidth: 400, enableHiding: false, elements: [ { value: 'Not a planet', label: 'Not a planet' }, @@ -197,7 +194,6 @@ export const fields = [ { header: 'Description', id: 'description', - maxWidth: 200, enableSorting: false, enableGlobalSearch: true, }, diff --git a/packages/dataviews/src/stories/index.story.js b/packages/dataviews/src/stories/index.story.js index c04e89a92baa85..8a5ccd83450237 100644 --- a/packages/dataviews/src/stories/index.story.js +++ b/packages/dataviews/src/stories/index.story.js @@ -39,6 +39,20 @@ Default.args = { [ LAYOUT_TABLE ]: { layout: { primaryField: 'title', + styles: { + image: { + width: 50, + }, + title: { + maxWidth: 400, + }, + type: { + maxWidth: 400, + }, + description: { + maxWidth: 200, + }, + }, }, }, [ LAYOUT_GRID ]: { diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 61d75d92da27d2..de2dcf027c2060 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -91,7 +91,7 @@ padding: $grid-unit-15; white-space: nowrap; - &[data-field-id="actions"] { + &.dataviews-view-table__actions-column { text-align: right; } @@ -215,6 +215,10 @@ } } } + + .components-v-stack > .dataviews-view-table__cell-content-wrapper:not(:first-child) { + min-height: 0; + } } .dataviews-view-table-header-button { padding: $grid-unit-05 $grid-unit-10; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 9cc9ffb0f76c0c..8e2626245682e6 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -75,21 +75,6 @@ export type Field< Item > = { */ render?: ( args: { item: Item } ) => ReactNode; - /** - * The width of the field column. - */ - width?: string | number; - - /** - * The minimum width of the field column. - */ - maxWidth?: string | number; - - /** - * The maximum width of the field column. - */ - minWidth?: string | number; - /** * Whether the field is sortable. */ @@ -249,11 +234,44 @@ interface ViewBase { perPage?: number; /** - * The hidden fields. + * The fields to render */ fields?: string[]; } +export interface CombinedField { + id: string; + + header: string; + + /** + * The fields to use as columns. + */ + children: string[]; + + /** + * The direction of the stack. + */ + direction: 'horizontal' | 'vertical'; +} + +export interface ColumnStyle { + /** + * The width of the field column. + */ + width?: string | number; + + /** + * The minimum width of the field column. + */ + maxWidth?: string | number; + + /** + * The maximum width of the field column. + */ + minWidth?: string | number; +} + export interface ViewTable extends ViewBase { type: 'table'; @@ -264,9 +282,14 @@ export interface ViewTable extends ViewBase { primaryField?: string; /** - * The field to use as the media field. + * The fields to use as columns. */ - mediaField?: string; + combinedFields?: CombinedField[]; + + /** + * The styles for the columns. + */ + styles?: Record< string, ColumnStyle >; }; } diff --git a/packages/dataviews/src/view-actions.tsx b/packages/dataviews/src/view-actions.tsx index fd9aff28b6479b..0493cfb5efea1f 100644 --- a/packages/dataviews/src/view-actions.tsx +++ b/packages/dataviews/src/view-actions.tsx @@ -20,7 +20,7 @@ import { cog } from '@wordpress/icons'; */ import { unlock } from './lock-unlock'; import { SORTING_DIRECTIONS, sortLabels } from './constants'; -import { VIEW_LAYOUTS } from './layouts'; +import { VIEW_LAYOUTS, getMandatoryFields } from './layouts'; import type { NormalizedField, View, SupportedLayouts } from './types'; const { @@ -147,10 +147,11 @@ function FieldsVisibilityMenu< Item >( { onChangeView, fields, }: FieldsVisibilityMenuProps< Item > ) { + const mandatoryFields = getMandatoryFields( view ); const hidableFields = fields.filter( ( field ) => field.enableHiding !== false && - field.id !== view?.layout?.mediaField + ! mandatoryFields.includes( field.id ) ); const viewFields = view.fields || fields.map( ( field ) => field.id ); if ( ! hidableFields?.length ) { diff --git a/packages/dataviews/src/view-table.tsx b/packages/dataviews/src/view-table.tsx index 1ba489ebe07ef5..f560c56fea183f 100644 --- a/packages/dataviews/src/view-table.tsx +++ b/packages/dataviews/src/view-table.tsx @@ -15,6 +15,8 @@ import { privateApis as componentsPrivateApis, CheckboxControl, Spinner, + __experimentalHStack as HStack, + __experimentalVStack as VStack, } from '@wordpress/components'; import { forwardRef, @@ -50,6 +52,7 @@ import type { SortDirection, ViewTable as ViewTableType, ViewTableProps, + CombinedField, } from './types'; import type { SetSelection } from './private-types'; @@ -63,7 +66,7 @@ const { } = unlock( componentsPrivateApis ); interface HeaderMenuProps< Item > { - field: NormalizedField< Item >; + fieldId: string; view: ViewTableType; fields: NormalizedField< Item >[]; onChangeView: ( view: ViewTableType ) => void; @@ -79,12 +82,35 @@ interface BulkSelectionCheckboxProps< Item > { getItemId: ( item: Item ) => string; } +interface TableColumnFieldProps< Item > { + primaryField?: NormalizedField< Item >; + field: NormalizedField< Item >; + item: Item; +} + +interface TableColumnCombinedProps< Item > { + primaryField?: NormalizedField< Item >; + fields: NormalizedField< Item >[]; + field: CombinedField; + item: Item; + view: ViewTableType; +} + +interface TableColumnProps< Item > { + primaryField?: NormalizedField< Item >; + fields: NormalizedField< Item >[]; + item: Item; + column: string; + view: ViewTableType; +} + interface TableRowProps< Item > { hasBulkActions: boolean; item: Item; actions: Action< Item >[]; + fields: NormalizedField< Item >[]; id: string; - visibleFields: NormalizedField< Item >[]; + view: ViewTableType; primaryField?: NormalizedField< Item >; selection: string[]; getItemId: ( item: Item ) => string; @@ -104,7 +130,7 @@ function WithDropDownMenuSeparators( { children }: { children: ReactNode } ) { const _HeaderMenu = forwardRef( function HeaderMenu< Item >( { - field, + fieldId, view, fields, onChangeView, @@ -113,6 +139,16 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item >( }: HeaderMenuProps< Item >, ref: Ref< HTMLButtonElement > ) { + const combinedField = view.layout?.combinedFields?.find( + ( f ) => f.id === fieldId + ); + if ( !! combinedField ) { + return combinedField.header; + } + const field = fields.find( ( f ) => f.id === fieldId ); + if ( ! field ) { + return null; + } const isHidable = field.enableHiding !== false; const isSortable = field.enableSorting !== false; const isSorted = view.sort?.field === field.id; @@ -227,7 +263,7 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item >( onChangeView( { ...view, fields: viewFields.filter( - ( fieldId ) => fieldId !== field.id + ( id ) => id !== field.id ), } ); } } @@ -292,12 +328,79 @@ function BulkSelectionCheckbox< Item >( { ); } +function TableColumn< Item >( { + column, + fields, + view, + ...props +}: TableColumnProps< Item > ) { + const field = fields.find( ( f ) => f.id === column ); + if ( !! field ) { + return ; + } + const combinedField = view.layout?.combinedFields?.find( + ( f ) => f.id === column + ); + if ( !! combinedField ) { + return ( + + ); + } + + return null; +} + +function TableColumnField< Item >( { + primaryField, + item, + field, +}: TableColumnFieldProps< Item > ) { + const value = field.render( { + item, + } ); + return ( + !! value && ( +
+ { value } +
+ ) + ); +} + +function TableColumnCombined< Item >( { + field, + ...props +}: TableColumnCombinedProps< Item > ) { + const children = field.children.map( ( child ) => ( + + ) ); + + if ( field.direction === 'horizontal' ) { + return { children }; + } + return { children }; +} + function TableRow< Item >( { hasBulkActions, item, actions, + fields, id, - visibleFields, + view, primaryField, selection, getItemId, @@ -305,13 +408,11 @@ function TableRow< Item >( { }: TableRowProps< Item > ) { const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); const isSelected = hasPossibleBulkAction && selection.includes( id ); - const [ isHovered, setIsHovered ] = useState( false ); const handleMouseEnter = () => { setIsHovered( true ); }; - const handleMouseLeave = () => { setIsHovered( false ); }; @@ -320,6 +421,7 @@ function TableRow< Item >( { // `onClick` and can be used to exclude touchscreen devices from certain // behaviours. const isTouchDevice = useRef( false ); + const columns = view.fields || fields.map( ( f ) => f.id ); return ( ( { ) } - { visibleFields.map( ( field ) => ( - -
- { field.render( { - item, - } ) } -
- - ) ) } + { columns.map( ( column: string ) => { + // Explicits picks the supported styles. + const { width, maxWidth, minWidth } = + view.layout?.styles?.[ column ] ?? {}; + + return ( + + + + ); + } ) } { !! actions?.length && ( // Disable reason: we are not making the element interactive, // but preventing any click events from bubbling up to the @@ -459,12 +554,7 @@ function ViewTable< Item >( { setNextHeaderMenuToFocus( fallback?.node ); }; - const viewFields = view.fields || fields.map( ( f ) => f.id ); - const visibleFields = fields.filter( - ( field ) => - viewFields.includes( field.id ) || - [ view.layout?.mediaField ].includes( field.id ) - ); + const columns = view.fields || fields.map( ( f ) => f.id ); const hasData = !! data?.length; const primaryField = fields.find( @@ -486,7 +576,6 @@ function ViewTable< Item >( { style={ { width: '1%', } } - data-field-id="selection" scope="col" > ( { /> ) } - { visibleFields.map( ( field, index ) => ( - - { - if ( node ) { - headerMenuRefs.current.set( - field.id, - { - node, - fallback: - visibleFields[ - index > 0 - ? index - 1 - : 1 - ]?.id, - } - ); - } else { - headerMenuRefs.current.delete( - field.id - ); - } - } } - field={ field } - view={ view } - fields={ fields } - onChangeView={ onChangeView } - onHide={ onHide } - setOpenedFilter={ setOpenedFilter } - /> - - ) ) } + { columns.map( ( column, index ) => { + // Explicits picks the supported styles. + const { width, maxWidth, minWidth } = + view.layout?.styles?.[ column ] ?? {}; + return ( + + { + if ( node ) { + headerMenuRefs.current.set( + column, + { + node, + fallback: + columns[ + index > 0 + ? index - 1 + : 1 + ], + } + ); + } else { + headerMenuRefs.current.delete( + column + ); + } + } } + fieldId={ column } + view={ view } + fields={ fields } + onChangeView={ onChangeView } + onHide={ onHide } + setOpenedFilter={ setOpenedFilter } + /> + + ); + } ) } { !! actions?.length && ( - + { __( 'Actions' ) } @@ -564,8 +650,9 @@ function ViewTable< Item >( { item={ item } hasBulkActions={ hasBulkActions } actions={ actions } + fields={ fields } id={ getItemId( item ) || index.toString() } - visibleFields={ visibleFields } + view={ view } primaryField={ primaryField } selection={ selection } getItemId={ getItemId } diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index aa126b4129c31a..6304c89d711fc6 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -62,6 +62,14 @@ const defaultLayouts = { [ LAYOUT_TABLE ]: { layout: { primaryField: 'title', + styles: { + preview: { + width: '1%', + }, + author: { + width: '1%', + }, + }, }, }, [ LAYOUT_GRID ]: { @@ -282,7 +290,6 @@ export default function DataviewsPatterns() { ), enableSorting: false, - width: '1%', }, { header: __( 'Title' ), @@ -335,7 +342,6 @@ export default function DataviewsPatterns() { filterBy: { isPrimary: true, }, - width: '1%', } ); } diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index eb404958493bad..0e3c725a06f792 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -6,12 +6,7 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { - Icon, - __experimentalText as Text, - __experimentalHStack as HStack, - VisuallyHidden, -} from '@wordpress/components'; +import { Icon, __experimentalHStack as HStack } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; @@ -56,11 +51,34 @@ const EMPTY_ARRAY = []; const defaultLayouts = { [ LAYOUT_TABLE ]: { + fields: [ 'template', 'author' ], layout: { primaryField: 'title', + combinedFields: [ + { + id: 'template', + header: __( 'Template' ), + children: [ 'title', 'description' ], + direction: 'vertical', + }, + ], + styles: { + template: { + maxWidth: 400, + minWidth: 320, + }, + preview: { + minWidth: 120, + maxWidth: 120, + }, + author: { + width: '1%', + }, + }, }, }, [ LAYOUT_GRID ]: { + fields: [ 'title', 'description', 'author' ], layout: { mediaField: 'preview', primaryField: 'title', @@ -68,6 +86,7 @@ const defaultLayouts = { }, }, [ LAYOUT_LIST ]: { + fields: [ 'title', 'description', 'author' ], layout: { primaryField: 'title', mediaField: 'preview', @@ -84,7 +103,7 @@ const DEFAULT_VIEW = { field: 'title', direction: 'asc', }, - fields: [ 'title', 'description', 'author' ], + fields: defaultLayouts[ LAYOUT_GRID ].fields, layout: defaultLayouts[ LAYOUT_GRID ].layout, filters: [], }; @@ -198,6 +217,7 @@ export default function PageTemplates() { ...DEFAULT_VIEW, type: usedType, layout: defaultLayouts[ usedType ].layout, + fields: defaultLayouts[ usedType ].fields, filters: activeView !== 'all' ? [ @@ -269,8 +289,6 @@ export default function PageTemplates() { render: ( { item } ) => { return ; }, - minWidth: 120, - maxWidth: 120, enableSorting: false, }, { @@ -280,7 +298,6 @@ export default function PageTemplates() { render: ( { item } ) => ( ), - maxWidth: 400, enableHiding: false, enableGlobalSearch: true, }, @@ -288,25 +305,14 @@ export default function PageTemplates() { header: __( 'Description' ), id: 'description', render: ( { item } ) => { - return item.description ? ( - <span className="page-templates-description"> - { decodeEntities( item.description ) } - </span> - ) : ( - view.type === LAYOUT_TABLE && ( - <> - <Text variant="muted" aria-hidden="true"> - — - </Text> - <VisuallyHidden> - { __( 'No description.' ) } - </VisuallyHidden> - </> + return ( + item.description && ( + <span className="page-templates-description"> + { decodeEntities( item.description ) } + </span> ) ); }, - maxWidth: 400, - minWidth: 320, enableSorting: false, enableGlobalSearch: true, }, @@ -318,7 +324,6 @@ export default function PageTemplates() { return <AuthorField viewType={ view.type } item={ item } />; }, elements: authors, - width: '1%', }, ], [ authors, view.type ] diff --git a/packages/edit-site/src/components/posts-app/posts-list.js b/packages/edit-site/src/components/posts-app/posts-list.js index 4ef9c5ce995230..d680f3abbf39d3 100644 --- a/packages/edit-site/src/components/posts-app/posts-list.js +++ b/packages/edit-site/src/components/posts-app/posts-list.js @@ -62,6 +62,24 @@ import { usePrevious } from '@wordpress/compose'; const { usePostActions } = unlock( editorPrivateApis ); const { useLocation, useHistory } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; +const defaultLayouts = { + [ LAYOUT_TABLE ]: { + layout: { + 'featured-image': { + width: '1%', + }, + title: { + maxWidth: 300, + }, + }, + }, + [ LAYOUT_GRID ]: { + layout: {}, + }, + [ LAYOUT_LIST ]: { + layout: {}, + }, +}; const getFormattedDate = ( dateToDisplay ) => dateI18n( @@ -401,7 +419,6 @@ export default function PostsList( { postType } ) { <FeaturedImage item={ item } viewType={ view.type } /> ), enableSorting: false, - width: '1%', }, { header: __( 'Title' ), @@ -455,7 +472,6 @@ export default function PostsList( { postType } ) { </HStack> ); }, - maxWidth: 300, enableHiding: false, }, { @@ -637,6 +653,7 @@ export default function PostsList( { postType } ) { setSelection={ setSelection } onSelectionChange={ onSelectionChange } getItemId={ getItemId } + defaultLayouts={ defaultLayouts } /> </Page> ); diff --git a/test/e2e/specs/site-editor/new-templates-list.spec.js b/test/e2e/specs/site-editor/new-templates-list.spec.js index 7273e3169db34e..92edc741011834 100644 --- a/test/e2e/specs/site-editor/new-templates-list.spec.js +++ b/test/e2e/specs/site-editor/new-templates-list.spec.js @@ -89,11 +89,11 @@ test.describe( 'Templates', () => { await page.getByRole( 'button', { name: 'Layout' } ).click(); await page.getByRole( 'menuitemradio', { name: 'Table' } ).click(); - await page.getByRole( 'button', { name: 'Description' } ).click(); + await page.getByRole( 'button', { name: 'Author' } ).click(); await page.getByRole( 'menuitem', { name: 'Hide' } ).click(); await expect( - page.getByRole( 'button', { name: 'Description' } ) + page.getByRole( 'button', { name: 'Author' } ) ).toBeHidden(); } ); } );