diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 022e160034866a..38c640da4021e2 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -6,6 +6,7 @@ ### Breaking Changes - Replace the `hiddenFields` property in the view prop of `DataViews` with a `fields` property that accepts an array of visible fields instead. +- Remove the `layout` property in the view prop of `DataViews`. The format is now determined by the `view.fields` property. Each field can be a string or an object with a `format` property. ### New features diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 1c3becf9a1e5b4..c67859f004482a 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -150,7 +150,6 @@ const view = { direction: 'desc', }, fields: [ 'author', 'status' ], - layout: {}, }; ``` @@ -167,10 +166,7 @@ Properties: - `sort`: - `field`: the field used for sorting the dataset. - `direction`: the direction to use for sorting, one of `asc` or `desc`. -- `fields`: the `id` of the fields that are visible in the UI. -- `layout`: config that is specific to a particular layout type. - - `mediaField`: used by the `grid` and `list` layouts. The `id` of the field to be used for rendering each card's media. - - `primaryField`: used by the `table`, `grid` and `list` layouts. The `id` of the field to be highlighted in each row/card/item. +- `fields`: the `id` of the fields that are visible in the UI. Can also be an object to define the render type of the field example: `{ field: 'author', format: 'media' }`. ### `onChangeView`: `function` @@ -200,7 +196,6 @@ function MyCustomPageTable() { }, ], fields: [ 'author', 'status' ], - layout: {}, } ); const queryArgs = useMemo( () => { diff --git a/packages/dataviews/src/dataviews.tsx b/packages/dataviews/src/dataviews.tsx index 8dd81e9013af8e..3c8fec9cc83a46 100644 --- a/packages/dataviews/src/dataviews.tsx +++ b/packages/dataviews/src/dataviews.tsx @@ -24,7 +24,13 @@ import { } from './bulk-actions'; import { normalizeFields } from './normalize-fields'; import BulkActionsToolbar from './bulk-actions-toolbar'; -import type { Action, Field, View, ViewBaseProps } from './types'; +import type { + Action, + Field, + FieldRenderConfig, + View, + ViewProps, +} from './types'; import type { SetSelection, SelectionOrUpdater } from './private-types'; type ItemWithId = { id: string }; @@ -46,6 +52,7 @@ type DataViewsProps< Item > = { selection?: string[]; setSelection?: SetSelection; onSelectionChange?: ( items: Item[] ) => void; + defaultFields?: Record< string, FieldRenderConfig[] >; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); @@ -69,6 +76,7 @@ export default function DataViews< Item >( { selection: selectionProperty, setSelection: setSelectionProperty, onSelectionChange = defaultOnSelectionChange, + defaultFields, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); const isUncontrolled = @@ -89,7 +97,7 @@ export default function DataViews< Item >( { } const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ) - ?.component as ComponentType< ViewBaseProps< Item > >; + ?.component as ComponentType< ViewProps< Item > >; const _fields = useMemo( () => normalizeFields( fields ), [ fields ] ); const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction( @@ -143,6 +151,7 @@ export default function DataViews< Item >( { view={ view } onChangeView={ onChangeView } supportedLayouts={ supportedLayouts } + defaultFields={ defaultFields } /> { + field: NormalizedField< Item >; + item: Item; + id?: string; +} + +export default function FieldFormatPrimary< Item >( { + field, + item, + id, +}: FieldFormatPrimaryProps< Item > ) { + return ( +
+ { field.render( { item } ) } +
+ ); +} diff --git a/packages/dataviews/src/normalize-field-render-configs.tsx b/packages/dataviews/src/normalize-field-render-configs.tsx new file mode 100644 index 00000000000000..b36e42df738af1 --- /dev/null +++ b/packages/dataviews/src/normalize-field-render-configs.tsx @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import type { + FieldRenderConfig, + NormalizedField, + NormalizedFieldRenderConfig, +} from './types'; + +/** + * Normalizes all the default field render configs + * To simplify its usage in the code base. + * + * @param configs Field Render Configs. + * @param fields Fields config. + * @return Normalized field render configs. + */ +export function normalizeFieldRenderConfigs( + configs: FieldRenderConfig[] | undefined, + fields: NormalizedField< any >[] +): NormalizedFieldRenderConfig[] { + return configs + ? configs.map( ( config ) => { + if ( typeof config === 'string' ) { + return { format: 'default', field: config }; + } + + return config; + } ) + : fields.map( ( f ) => ( { format: 'default', field: f.id } ) ); +} diff --git a/packages/dataviews/src/stories/index.story.js b/packages/dataviews/src/stories/index.story.js index bf4c1e1b1dba86..cad476077d61e5 100644 --- a/packages/dataviews/src/stories/index.story.js +++ b/packages/dataviews/src/stories/index.story.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useState, useMemo, useCallback } from '@wordpress/element'; +import { useState, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -17,14 +17,18 @@ const meta = { }; export default meta; -const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: { - primaryField: 'title', - }, - [ LAYOUT_GRID ]: { - mediaField: 'image', - primaryField: 'title', - }, +const defaultFields = { + [ LAYOUT_TABLE ]: [ + { format: 'primary', field: 'title' }, + 'description', + 'categories', + ], + [ LAYOUT_GRID ]: [ + { format: 'media', field: 'image' }, + { format: 'primary', field: 'title' }, + 'description', + 'categories', + ], }; export const Default = ( props ) => { @@ -32,21 +36,6 @@ export const Default = ( props ) => { const { data: shownData, paginationInfo } = useMemo( () => { return filterSortAndPaginate( data, view, fields ); }, [ view ] ); - const onChangeView = useCallback( - ( newView ) => { - if ( newView.type !== view.type ) { - newView = { - ...newView, - layout: { - ...defaultConfigPerViewType[ newView.type ], - }, - }; - } - - setView( newView ); - }, - [ view.type, setView ] - ); return ( { data={ shownData } view={ view } fields={ fields } - onChangeView={ onChangeView } + onChangeView={ setView } + defaultFields={ defaultFields } /> ); }; diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index ae4fbed9cfada9..1d96977e178b0b 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -205,7 +205,7 @@ flex-grow: 1; } - &.dataviews-view-table__primary-field { + .dataviews-field-format-primary { a { flex-grow: 0; } @@ -246,9 +246,7 @@ } } -.dataviews-view-list__primary-field, -.dataviews-view-grid__primary-field, -.dataviews-view-table__primary-field { +.dataviews-field-format-primary { font-size: $default-font-size; font-weight: 500; color: $gray-700; @@ -312,10 +310,8 @@ .dataviews-view-grid__title-actions { padding: $grid-unit-10 0 $grid-unit-05; - } - - .dataviews-view-grid__primary-field { - min-height: $grid-unit-40; // Preserve layout when there is no ellipsis button + // Preserve layout when there is no ellipsis button + min-height: $grid-unit-40; } &.is-selected { @@ -446,7 +442,7 @@ } &:not(.is-selected) { - .dataviews-view-list__primary-field { + .dataviews-field-format-primary { color: $gray-900; } &:hover, @@ -454,7 +450,7 @@ color: var(--wp-admin-theme-color); background-color: #f8f8f8; - .dataviews-view-list__primary-field, + .dataviews-field-format-primary, .dataviews-view-list__fields { color: var(--wp-admin-theme-color); } @@ -469,7 +465,7 @@ background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04); color: $gray-900; - .dataviews-view-list__primary-field, + .dataviews-field-format-primary, .dataviews-view-list__fields { color: var(--wp-admin-theme-color); } @@ -493,7 +489,7 @@ border-radius: $radius-block-ui; } } - .dataviews-view-list__primary-field { + .dataviews-field-format-primary { min-height: $grid-unit-05 * 5; line-height: $grid-unit-05 * 5; overflow: hidden; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 36dcfb2e29f35d..b8c006fde76b30 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -207,11 +207,23 @@ export interface NormalizedFilter { isPrimary: boolean; } -interface ViewBase { +export interface NormalizedFieldRenderConfig { + format: 'primary' | 'badge' | 'media' | 'column' | 'default'; + field: string; +} + +export type FieldRenderConfig = + | string + | { + format: 'primary' | 'badge' | 'media' | 'column' | 'default'; + field: string; + }; + +export interface View { /** * The layout of the view. */ - type: string; + type: 'table' | 'grid' | 'list'; /** * The global search term. @@ -251,69 +263,9 @@ interface ViewBase { /** * The hidden fields. */ - fields?: string[]; + fields?: FieldRenderConfig[]; } -export interface ViewTable extends ViewBase { - type: 'table'; - - layout: { - /** - * The field to use as the primary field. - */ - primaryField?: string; - - /** - * The field to use as the media field. - */ - mediaField?: string; - }; -} - -export interface ViewList extends ViewBase { - type: 'list'; - - layout: { - /** - * The field to use as the primary field. - */ - primaryField?: string; - - /** - * The field to use as the media field. - */ - mediaField?: string; - }; -} - -export interface ViewGrid extends ViewBase { - type: 'grid'; - - layout: { - /** - * The field to use as the primary field. - */ - primaryField?: string; - - /** - * The field to use as the media field. - */ - mediaField?: string; - - /** - * The fields to use as columns. - */ - columnFields?: string[]; - - /** - * The fields to use as badge fields. - */ - badgeFields?: string[]; - }; -} - -export type View = ViewList | ViewGrid | ViewTable; - interface ActionBase< Item > { /** * The unique identifier of the action. @@ -400,7 +352,7 @@ export interface ActionButton< Item > extends ActionBase< Item > { export type Action< Item > = ActionModal< Item > | ActionButton< Item >; -export interface ViewBaseProps< Item > { +export interface ViewProps< Item > { actions: Action< Item >[]; data: Item[]; fields: NormalizedField< Item >[]; @@ -412,20 +364,3 @@ export interface ViewBaseProps< Item > { setOpenedFilter: ( fieldId: string ) => void; view: View; } - -export interface ViewTableProps< Item > extends ViewBaseProps< Item > { - view: ViewTable; -} - -export interface ViewListProps< Item > extends ViewBaseProps< Item > { - view: ViewList; -} - -export interface ViewGridProps< Item > extends ViewBaseProps< Item > { - view: ViewGrid; -} - -export type ViewProps< Item > = - | ViewTableProps< Item > - | ViewGridProps< Item > - | ViewListProps< Item >; diff --git a/packages/dataviews/src/view-actions.tsx b/packages/dataviews/src/view-actions.tsx index 44add039efd7e1..896c454d42cf4c 100644 --- a/packages/dataviews/src/view-actions.tsx +++ b/packages/dataviews/src/view-actions.tsx @@ -20,7 +20,8 @@ import { cog } from '@wordpress/icons'; import { unlock } from './lock-unlock'; import { SORTING_DIRECTIONS, sortLabels } from './constants'; import { VIEW_LAYOUTS } from './layouts'; -import type { NormalizedField, View } from './types'; +import type { FieldRenderConfig, NormalizedField, View } from './types'; +import { normalizeFieldRenderConfigs } from './normalize-field-render-configs'; const { DropdownMenuV2: DropdownMenu, @@ -35,6 +36,7 @@ interface ViewTypeMenuProps { view: View; onChangeView: ( view: View ) => void; supportedLayouts?: string[]; + defaultFields?: Record< string, FieldRenderConfig[] >; } interface PageSizeMenuProps { @@ -46,6 +48,7 @@ interface FieldsVisibilityMenuProps< Item > { view: View; onChangeView: ( view: View ) => void; fields: NormalizedField< Item >[]; + defaultFields?: Record< string, FieldRenderConfig[] >; } interface SortMenuProps< Item > { @@ -59,12 +62,14 @@ interface ViewActionsProps< Item > { view: View; onChangeView: ( view: View ) => void; supportedLayouts?: string[]; + defaultFields?: Record< string, FieldRenderConfig[] >; } function ViewTypeMenu( { view, onChangeView, supportedLayouts, + defaultFields, }: ViewTypeMenuProps ) { let _availableViews = VIEW_LAYOUTS; if ( supportedLayouts ) { @@ -106,7 +111,9 @@ function ViewTypeMenu( { return onChangeView( { ...view, type: e.target.value, - layout: {}, + fields: + defaultFields?.[ e.target.value ] ?? + undefined, } ); } throw new Error( 'Invalid dataview' ); @@ -165,15 +172,25 @@ function FieldsVisibilityMenu< Item >( { view, onChangeView, fields, + defaultFields, }: FieldsVisibilityMenuProps< Item > ) { + const fieldRenderConfigs = normalizeFieldRenderConfigs( + view.fields, + fields + ); + const mediaFieldId = fieldRenderConfigs.find( + ( fieldRender ) => fieldRender.format === 'media' + )?.field; const hidableFields = fields.filter( - ( field ) => - field.enableHiding !== false && field.id !== view.layout.mediaField + ( field ) => field.enableHiding !== false && field.id !== mediaFieldId ); - const viewFields = view.fields || fields.map( ( field ) => field.id ); if ( ! hidableFields?.length ) { return null; } + const normalizedDefaultFields = normalizeFieldRenderConfigs( + defaultFields?.[ view.type ], + [] + ); return ( ( { } > { hidableFields?.map( ( field ) => { + const isVisible = !! fieldRenderConfigs.find( + ( f ) => f.field === field.id + ); return ( { + const fieldFormat = normalizedDefaultFields.find( + ( f ) => f.field === field.id + ); onChangeView( { ...view, - fields: viewFields.includes( field.id ) - ? viewFields.filter( - ( id ) => id !== field.id + fields: isVisible + ? fieldRenderConfigs.filter( + ( fieldRender ) => + fieldRender.field !== field.id ) - : [ ...viewFields, field.id ], + : [ + ...fieldRenderConfigs, + fieldFormat ?? field.id, + ], } ); } } > @@ -304,6 +331,7 @@ function _ViewActions< Item >( { view, onChangeView, supportedLayouts, + defaultFields, }: ViewActionsProps< Item > ) { return ( ( { view={ view } onChangeView={ onChangeView } supportedLayouts={ supportedLayouts } + defaultFields={ defaultFields } /> ( { fields={ fields } view={ view } onChangeView={ onChangeView } + defaultFields={ defaultFields } /> diff --git a/packages/dataviews/src/view-grid.tsx b/packages/dataviews/src/view-grid.tsx index 7406cd9c7818af..203a5bb0ab1984 100644 --- a/packages/dataviews/src/view-grid.tsx +++ b/packages/dataviews/src/view-grid.tsx @@ -22,20 +22,26 @@ import { __ } from '@wordpress/i18n'; import ItemActions from './item-actions'; import SingleSelectionCheckbox from './single-selection-checkbox'; import { useHasAPossibleBulkAction } from './bulk-actions'; -import type { Action, NormalizedField, ViewGridProps } from './types'; +import type { + Action, + NormalizedField, + NormalizedFieldRenderConfig, + ViewProps, +} from './types'; import type { SetSelection } from './private-types'; +import { normalizeFieldRenderConfigs } from './normalize-field-render-configs'; +import FieldFormatPrimary from './field-format-primary'; interface GridItemProps< Item > { selection: string[]; onSelectionChange: SetSelection; getItemId: ( item: Item ) => string; item: Item; + fields: NormalizedField< Item >[]; actions: Action< Item >[]; mediaField?: NormalizedField< Item >; primaryField?: NormalizedField< Item >; - visibleFields: NormalizedField< Item >[]; - badgeFields: NormalizedField< Item >[]; - columnFields?: string[]; + groupedRenderFields: Record< string, NormalizedFieldRenderConfig[] >; } function GridItem< Item >( { @@ -44,15 +50,16 @@ function GridItem< Item >( { getItemId, item, actions, + fields, mediaField, primaryField, - visibleFields, - badgeFields, - columnFields, + groupedRenderFields, }: GridItemProps< Item > ) { const hasBulkAction = useHasAPossibleBulkAction( actions, item ); const id = getItemId( item ); const isSelected = selection.includes( id ); + const badgeFields = groupedRenderFields.badge; + const otherFields = groupedRenderFields.other; return ( ( { ( { primaryField={ primaryField } disabled={ ! hasBulkAction } /> - - { primaryField?.render( { item } ) } - + { !! primaryField && ( + + ) } { !! badgeFields?.length && ( @@ -103,7 +111,13 @@ function GridItem< Item >( { alignment="top" justify="flex-start" > - { badgeFields.map( ( field ) => { + { fields.map( ( field ) => { + const isVisible = !! badgeFields.find( + ( fr ) => field.id === fr.field + ); + if ( ! isVisible ) { + return null; + } const renderedValue = field.render( { item, } ); @@ -121,9 +135,15 @@ function GridItem< Item >( { } ) } ) } - { !! visibleFields?.length && ( + { !! otherFields?.length && ( - { visibleFields.map( ( field ) => { + { fields.map( ( field ) => { + const fieldRenderConfig = otherFields.find( + ( fr ) => field.id === fr.field + ); + if ( ! fieldRenderConfig ) { + return null; + } const renderedValue = field.render( { item, } ); @@ -134,7 +154,7 @@ function GridItem< Item >( { ( { expanded style={ { height: 'auto' } } direction={ - columnFields?.includes( field.id ) + fieldRenderConfig.format === 'column' ? 'column' : 'row' } @@ -178,33 +198,34 @@ export default function ViewGrid< Item >( { onSelectionChange, selection, view, -}: ViewGridProps< Item > ) { - const mediaField = fields.find( - ( field ) => field.id === view.layout.mediaField - ); - const primaryField = fields.find( - ( field ) => field.id === view.layout.primaryField +}: ViewProps< Item > ) { + const fieldRenderConfigs = normalizeFieldRenderConfigs( + view.fields, + fields ); - const viewFields = view.fields || fields.map( ( field ) => field.id ); - const { visibleFields, badgeFields } = fields.reduce( - ( accumulator: Record< string, NormalizedField< Item >[] >, field ) => { + const mediaFieldId = fieldRenderConfigs.find( + ( fieldRender ) => fieldRender.format === 'media' + )?.field; + const primaryFieldId = fieldRenderConfigs.find( + ( fieldRender ) => fieldRender.format === 'primary' + )?.field; + const mediaField = fields.find( ( f ) => f.id === mediaFieldId ); + const primaryField = fields.find( ( f ) => f.id === primaryFieldId ); + const groupedFields = fieldRenderConfigs.reduce( + ( + accumulator: Record< string, NormalizedFieldRenderConfig[] >, + fieldRender + ) => { if ( - ! viewFields.includes( field.id ) || - [ view.layout.mediaField, view.layout.primaryField ].includes( - field.id - ) + [ mediaFieldId, primaryFieldId ].includes( fieldRender.field ) ) { return accumulator; } - // If the field is a badge field, add it to the badgeFields array - // otherwise add it to the rest visibleFields array. - const key = view.layout.badgeFields?.includes( field.id ) - ? 'badgeFields' - : 'visibleFields'; - accumulator[ key ].push( field ); + const key = fieldRender.format === 'badge' ? 'badge' : 'other'; + accumulator[ key ].push( fieldRender ); return accumulator; }, - { visibleFields: [], badgeFields: [] } + { other: [], badge: [] } ); const hasData = !! data?.length; return ( @@ -226,11 +247,10 @@ export default function ViewGrid< Item >( { getItemId={ getItemId } item={ item } actions={ actions } + fields={ fields } mediaField={ mediaField } primaryField={ primaryField } - visibleFields={ visibleFields } - badgeFields={ badgeFields } - columnFields={ view.layout.columnFields } + groupedRenderFields={ groupedFields } /> ); } ) } diff --git a/packages/dataviews/src/view-list.tsx b/packages/dataviews/src/view-list.tsx index 6a56f05e83202c..5d3182c7eea799 100644 --- a/packages/dataviews/src/view-list.tsx +++ b/packages/dataviews/src/view-list.tsx @@ -33,12 +33,20 @@ import { useRegistry } from '@wordpress/data'; * Internal dependencies */ import { unlock } from './lock-unlock'; -import type { Action, NormalizedField, ViewListProps } from './types'; +import type { + Action, + NormalizedField, + NormalizedFieldRenderConfig, + ViewProps, +} from './types'; import { ActionsDropdownMenuGroup, ActionModal } from './item-actions'; +import { normalizeFieldRenderConfigs } from './normalize-field-render-configs'; +import FieldFormatPrimary from './field-format-primary'; interface ListViewItemProps< Item > { actions: Action< Item >[]; + fields: NormalizedField< Item >[]; id?: string; isSelected: boolean; item: Item; @@ -46,7 +54,7 @@ interface ListViewItemProps< Item > { onSelect: ( item: Item ) => void; primaryField?: NormalizedField< Item >; store: CompositeStore; - visibleFields: NormalizedField< Item >[]; + otherFields: NormalizedFieldRenderConfig[]; } const { @@ -59,6 +67,7 @@ const { function ListItem< Item >( { actions, + fields, id, isSelected, item, @@ -66,7 +75,7 @@ function ListItem< Item >( { onSelect, primaryField, store, - visibleFields, + otherFields, }: ListViewItemProps< Item > ) { const registry = useRegistry(); const itemRef = useRef< HTMLElement >( null ); @@ -153,32 +162,41 @@ function ListItem< Item >( { ) } - - { primaryField?.render( { item } ) } - + { !! primaryField && ( + + ) }
- { visibleFields.map( ( field ) => ( -
- { + const isVisible = !! otherFields.find( + ( fr ) => field.id === fr.field + ); + if ( ! isVisible ) { + return null; + } + return ( +
- { field.header } - - - { field.render( { item } ) } - -
- ) ) } + + { field.header } + + + { field.render( { item } ) } + +
+ ); + } ) }
@@ -303,7 +321,7 @@ function ListItem< Item >( { ); } -export default function ViewList< Item >( props: ViewListProps< Item > ) { +export default function ViewList< Item >( props: ViewProps< Item > ) { const { actions, data, @@ -318,20 +336,21 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { const selectedItem = data?.findLast( ( item ) => selection.includes( getItemId( item ) ) ); - - const mediaField = fields.find( - ( field ) => field.id === view.layout.mediaField - ); - const primaryField = fields.find( - ( field ) => field.id === view.layout.primaryField + const fieldRenderConfigs = normalizeFieldRenderConfigs( + view.fields, + fields ); - const viewFields = view.fields || fields.map( ( field ) => field.id ); - const visibleFields = fields.filter( - ( field ) => - viewFields.includes( field.id ) && - ! [ view.layout.primaryField, view.layout.mediaField ].includes( - field.id - ) + const mediaFieldId = fieldRenderConfigs.find( + ( fieldRender ) => fieldRender.format === 'media' + )?.field; + const primaryFieldId = fieldRenderConfigs.find( + ( fieldRender ) => fieldRender.format === 'primary' + )?.field; + const mediaField = fields.find( ( f ) => f.id === mediaFieldId ); + const primaryField = fields.find( ( f ) => f.id === primaryFieldId ); + const otherFields = fieldRenderConfigs.filter( + ( fieldRender ) => + ! [ mediaFieldId, primaryFieldId ].includes( fieldRender.field ) ); const onSelect = ( item: Item ) => @@ -396,13 +415,14 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { key={ id } id={ id } actions={ actions } + fields={ fields } item={ item } isSelected={ item === selectedItem } onSelect={ onSelect } mediaField={ mediaField } primaryField={ primaryField } store={ store } - visibleFields={ visibleFields } + otherFields={ otherFields } /> ); } ) } diff --git a/packages/dataviews/src/view-table.tsx b/packages/dataviews/src/view-table.tsx index 79118d2f095616..41b6d22af62ca2 100644 --- a/packages/dataviews/src/view-table.tsx +++ b/packages/dataviews/src/view-table.tsx @@ -48,10 +48,13 @@ import type { Action, NormalizedField, SortDirection, - ViewTable as ViewTableType, - ViewTableProps, + ViewProps, + View, + NormalizedFieldRenderConfig, } from './types'; import type { SetSelection } from './private-types'; +import FieldFormatPrimary from './field-format-primary'; +import { normalizeFieldRenderConfigs } from './normalize-field-render-configs'; const { DropdownMenuV2: DropdownMenu, @@ -64,9 +67,9 @@ const { interface HeaderMenuProps< Item > { field: NormalizedField< Item >; - view: ViewTableType; + view: View; fields: NormalizedField< Item >[]; - onChangeView: ( view: ViewTableType ) => void; + onChangeView: ( view: View ) => void; onHide: ( field: NormalizedField< Item > ) => void; setOpenedFilter: ( fieldId: string ) => void; } @@ -82,9 +85,10 @@ interface BulkSelectionCheckboxProps< Item > { interface TableRowProps< Item > { hasBulkActions: boolean; item: Item; + fields: NormalizedField< Item >[]; actions: Action< Item >[]; id: string; - visibleFields: NormalizedField< Item >[]; + fieldRenderConfigs: NormalizedFieldRenderConfig[]; primaryField?: NormalizedField< Item >; selection: string[]; getItemId: ( item: Item ) => string; @@ -296,30 +300,30 @@ function TableRow< Item >( { hasBulkActions, item, actions, + fields, id, - visibleFields, - primaryField, + fieldRenderConfigs, selection, getItemId, onSelectionChange, }: 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 ); }; - // Will be set to true if `onTouchStart` fires. This happens before // `onClick` and can be used to exclude touchscreen devices from certain // behaviours. const isTouchDevice = useRef( false ); + const primaryFieldId = fieldRenderConfigs.find( + ( fieldRenderConfig ) => fieldRenderConfig.format === 'primary' + )?.field; + const primaryField = fields.find( ( f ) => f.id === primaryFieldId ); return ( ( { ) } - { visibleFields.map( ( field ) => ( - -
{ + const fieldRender = fieldRenderConfigs.find( + ( fr ) => fr.field === field.id + ); + if ( ! fieldRender ) { + return null; + } + const fieldOutput = + fieldRender.format === 'primary' ? ( + + ) : ( + field.render( { item } ) + ); + + return ( + - { field.render( { - item, - } ) } -
- - ) ) } +
+ { fieldOutput } +
+ + ); + } ) } { !! actions?.length && ( // Disable reason: we are not making the element interactive, // but preventing any click events from bubbling up to the @@ -423,7 +432,7 @@ function ViewTable< Item >( { selection, setOpenedFilter, view, -}: ViewTableProps< Item > ) { +}: ViewProps< Item > ) { const headerMenuRefs = useRef< Map< string, { node: HTMLButtonElement; fallback: string } > >( new Map() ); @@ -459,16 +468,10 @@ 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 hasData = !! data?.length; - - const primaryField = fields.find( - ( field ) => field.id === view.layout.primaryField + const fieldRenderConfigs = normalizeFieldRenderConfigs( + view.fields, + fields ); return ( @@ -498,52 +501,63 @@ function ViewTable< Item >( { /> ) } - { 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 - ); - } + { fields.map( ( field ) => { + const fieldRenderIndex = + fieldRenderConfigs.findIndex( + ( fieldRender ) => + fieldRender.field === field.id + ); + if ( fieldRenderIndex === -1 ) { + return null; + } + const fallback = + fieldRenderConfigs[ + fieldRenderIndex > 0 + ? fieldRenderIndex - 1 + : 1 + ]?.field; + return ( + - - ) ) } + data-field-id={ field.id } + aria-sort={ + view.sort?.field === field.id + ? sortValues[ view.sort.direction ] + : undefined + } + scope="col" + > + { + if ( node ) { + headerMenuRefs.current.set( + field.id, + { + node, + fallback, + } + ); + } else { + headerMenuRefs.current.delete( + field.id + ); + } + } } + field={ field } + view={ view } + fields={ fields } + onChangeView={ onChangeView } + onHide={ onHide } + setOpenedFilter={ setOpenedFilter } + /> + + ); + } ) } { !! actions?.length && ( ( { item={ item } hasBulkActions={ hasBulkActions } actions={ actions } + fields={ fields } id={ getItemId( item ) || index.toString() } - visibleFields={ visibleFields } - primaryField={ primaryField } + fieldRenderConfigs={ fieldRenderConfigs } selection={ selection } getItemId={ getItemId } onSelectionChange={ onSelectionChange } diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index e9114e53184ccf..c70f2c9d474807 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -13,13 +13,7 @@ import { Flex, } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; -import { - useState, - useMemo, - useCallback, - useId, - useEffect, -} from '@wordpress/element'; +import { useState, useMemo, useId, useEffect } from '@wordpress/element'; import { BlockPreview, privateApis as blockEditorPrivateApis, @@ -64,25 +58,27 @@ const { usePostActions } = unlock( editorPrivateApis ); const { useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; -const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: { - primaryField: 'title', - }, - [ LAYOUT_GRID ]: { - mediaField: 'preview', - primaryField: 'title', - badgeFields: [ 'sync-status' ], - }, +const defaultFields = { + [ LAYOUT_TABLE ]: [ + 'preview', + { format: 'primary', field: 'title' }, + 'sync-status', + 'author', + ], + [ LAYOUT_GRID ]: [ + { format: 'media', field: 'preview' }, + { format: 'primary', field: 'title' }, + { format: 'badge', field: 'sync-status' }, + 'author', + ], }; const DEFAULT_VIEW = { type: LAYOUT_GRID, search: '', page: 1, perPage: 20, - layout: { - ...defaultConfigPerViewType[ LAYOUT_GRID ], - }, filters: [], + fields: defaultFields[ LAYOUT_GRID ], }; const SYNC_FILTERS = [ @@ -381,20 +377,6 @@ export default function DataviewsPatterns() { } return [ editAction, ...patternActions ].filter( Boolean ); }, [ editAction, type, templatePartActions, patternActions ] ); - const onChangeView = useCallback( - ( newView ) => { - if ( newView.type !== view.type ) { - newView = { - ...newView, - layout: { - ...defaultConfigPerViewType[ newView.type ], - }, - }; - } - setView( newView ); - }, - [ view.type, setView ] - ); const id = useId(); const settings = usePatternSettings(); // Wrap everything in a block editor provider. @@ -421,8 +403,9 @@ export default function DataviewsPatterns() { getItemId={ ( item ) => item.name ?? item.id } isLoading={ isResolving } view={ view } - onChangeView={ onChangeView } + onChangeView={ setView } supportedLayouts={ [ LAYOUT_GRID, LAYOUT_TABLE ] } + defaultFields={ defaultFields } /> diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 5ba29d25a432f5..2b88e47c8afdcc 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -54,19 +54,22 @@ const { useHistory, useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; -const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: { - primaryField: 'title', - }, - [ LAYOUT_GRID ]: { - mediaField: 'preview', - primaryField: 'title', - columnFields: [ 'description' ], - }, - [ LAYOUT_LIST ]: { - primaryField: 'title', - mediaField: 'preview', - }, +const defaultFields = { + [ LAYOUT_TABLE ]: [ + { format: 'primary', field: 'title' }, + 'description', + 'author', + ], + [ LAYOUT_GRID ]: [ + { format: 'media', field: 'preview' }, + { format: 'primary', field: 'title' }, + { format: 'column', field: 'description' }, + ], + [ LAYOUT_LIST ]: [ + { format: 'media', field: 'preview' }, + { format: 'primary', field: 'title' }, + 'author', + ], }; const DEFAULT_VIEW = { @@ -78,8 +81,7 @@ const DEFAULT_VIEW = { field: 'title', direction: 'asc', }, - fields: [ 'title', 'description', 'author' ], - layout: defaultConfigPerViewType[ LAYOUT_GRID ], + fields: defaultFields[ LAYOUT_GRID ], filters: [], }; @@ -192,7 +194,7 @@ export default function PageTemplates() { return { ...DEFAULT_VIEW, type: usedType, - layout: defaultConfigPerViewType[ usedType ], + fields: defaultFields[ usedType ], filters: activeView !== 'all' ? [ @@ -336,13 +338,6 @@ export default function PageTemplates() { const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { - newView = { - ...newView, - layout: { - ...defaultConfigPerViewType[ newView.type ], - }, - }; - history.push( { ...params, layout: newView.type, @@ -371,6 +366,7 @@ export default function PageTemplates() { onSelectionChange={ onSelectionChange } selection={ selection } setSelection={ setSelection } + defaultFields={ defaultFields } /> ); 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 cf9b6f43ef9cf1..b76770d010c0e3 100644 --- a/packages/edit-site/src/components/posts-app/posts-list.js +++ b/packages/edit-site/src/components/posts-app/posts-list.js @@ -25,7 +25,7 @@ import Page from '../page'; import { default as Link, useLink } from '../routes/link'; import { useDefaultViews, - DEFAULT_CONFIG_PER_VIEW_TYPE, + defaultFields, } from '../sidebar-dataviews/default-views'; import { LAYOUT_GRID, @@ -67,9 +67,6 @@ function useView( postType ) { return { ...defaultView, type: layout, - layout: { - ...( DEFAULT_CONFIG_PER_VIEW_TYPE[ layout ] || {} ), - }, }; } return defaultView; @@ -102,16 +99,7 @@ function useView( postType ) { const storedView = editedViewRecord?.content && JSON.parse( editedViewRecord?.content ); - if ( ! storedView ) { - return storedView; - } - - return { - ...storedView, - layout: { - ...( DEFAULT_CONFIG_PER_VIEW_TYPE[ storedView?.type ] || {} ), - }, - }; + return storedView; }, [ editedViewRecord?.content ] ); const setCustomView = useCallback( @@ -496,23 +484,6 @@ export default function PostsList( { postType } ) { () => [ editAction, ...postTypeActions ], [ postTypeActions, editAction ] ); - - const onChangeView = useCallback( - ( newView ) => { - if ( newView.type !== view.type ) { - newView = { - ...newView, - layout: { - ...DEFAULT_CONFIG_PER_VIEW_TYPE[ newView.type ], - }, - }; - } - - setView( newView ); - }, - [ view.type, setView ] - ); - const [ showAddPostModal, setShowAddPostModal ] = useState( false ); const openModal = () => setShowAddPostModal( true ); @@ -558,11 +529,12 @@ export default function PostsList( { postType } ) { data={ records || EMPTY_ARRAY } isLoading={ isLoadingMainEntities || isLoadingAuthors } view={ view } - onChangeView={ onChangeView } + onChangeView={ setView } selection={ selection } setSelection={ setSelection } onSelectionChange={ onSelectionChange } getItemId={ getItemId } + defaultFields={ defaultFields } /> ); 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 41d641d7bf454e..40539337382d32 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -26,18 +26,24 @@ import { OPERATOR_IS_ANY, } from '../../utils/constants'; -export const DEFAULT_CONFIG_PER_VIEW_TYPE = { - [ LAYOUT_TABLE ]: { - primaryField: 'title', - }, - [ LAYOUT_GRID ]: { - mediaField: 'featured-image', - primaryField: 'title', - }, - [ LAYOUT_LIST ]: { - primaryField: 'title', - mediaField: 'featured-image', - }, +export const defaultFields = { + [ LAYOUT_TABLE ]: [ + { format: 'primary', field: 'title' }, + 'author', + 'status', + ], + [ LAYOUT_GRID ]: [ + { format: 'media', field: 'featured-image' }, + { format: 'primary', field: 'title' }, + 'author', + 'status', + ], + [ LAYOUT_LIST ]: [ + { format: 'media', field: 'featured-image' }, + { format: 'primary', field: 'title' }, + 'author', + 'status', + ], }; const DEFAULT_POST_BASE = { @@ -50,10 +56,7 @@ const DEFAULT_POST_BASE = { field: 'date', direction: 'desc', }, - fields: [ 'title', 'author', 'status' ], - layout: { - ...DEFAULT_CONFIG_PER_VIEW_TYPE[ LAYOUT_LIST ], - }, + fields: defaultFields[ LAYOUT_LIST ], }; export function useDefaultViews( { postType } ) {