Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataViews: Adds a control to the views actions to switch layouts #55311

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 21 additions & 178 deletions packages/edit-site/src/components/dataviews/dataviews.js
Original file line number Diff line number Diff line change
@@ -1,211 +1,54 @@
/**
* External dependencies
*/
import {
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
getPaginationRowModel,
useReactTable,
} from '@tanstack/react-table';

/**
* WordPress dependencies
*/
import {
__experimentalVStack as VStack,
__experimentalHStack as HStack,
VisuallyHidden,
DropdownMenu,
MenuGroup,
MenuItem,
} from '@wordpress/components';
import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import ListView from './list-view';
import { Pagination } from './pagination';
import ViewList from './view-list';
import Pagination from './pagination';
import ViewActions from './view-actions';
import TextFilter from './text-filter';
import { moreVertical } from '@wordpress/icons';

const EMPTY_OBJECT = {};
import { ViewGrid } from './view-grid';

export default function DataViews( {
actions,
data,
fields,
view,
onChangeView,
isLoading,
fields,
actions,
data,
isLoading = false,
paginationInfo,
options: { pageCount },
} ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prop duplicated an info we already had in paginationInfo so I just removed it.

const columns = useMemo( () => {
const _columns = [ ...fields ];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically all the tanstack initialization have move to the "view-list.js" file instead because we don't need it in other views like Grid, Kanban...

Copy link
Contributor

@ntsekouras ntsekouras Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we don't need them in the other views? Do you think all the callbacks to change the view like sorting, per page, etc.. would be different in some way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I'm not entirely sure what you want to reuse here? by callbacks you mean things like onChangeView( { perPage: value } ) something else? These are very small callbacks that don't need to be shared.

Copy link
Contributor

@ntsekouras ntsekouras Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant the callbacks like onColumnVisibilityChange etc.. that use onChangeView. But I guess the whole point here is that we define our own API with fields, etc.., use them with setView and that tanstack can probably be more useful only in list view, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not by principle or something I'm fine reusing tanstack wherever I can, I'm just not seeing where it can be useful elsewhere. Let's move forward and see. Maybe I'm missing something.

if ( actions?.length ) {
_columns.push( {
header: <VisuallyHidden>{ __( 'Actions' ) }</VisuallyHidden>,
id: 'actions',
cell: ( props ) => {
return (
<DropdownMenu
icon={ moreVertical }
label={ __( 'Actions' ) }
>
{ () => (
<MenuGroup>
{ actions.map( ( action ) => (
<MenuItem
key={ action.id }
onClick={ () =>
action.perform(
props.row.original
)
}
isDestructive={
action.isDesctructive
}
>
{ action.label }
</MenuItem>
) ) }
</MenuGroup>
) }
</DropdownMenu>
);
},
enableHiding: false,
} );
}

return _columns;
}, [ fields, actions ] );

const columnVisibility = useMemo( () => {
if ( ! view.hiddenFields?.length ) {
return;
}
return view.hiddenFields.reduce(
( accumulator, fieldId ) => ( {
...accumulator,
[ fieldId ]: false,
} ),
{}
);
}, [ view.hiddenFields ] );

const dataView = useReactTable( {
data,
columns,
manualSorting: true,
manualFiltering: true,
manualPagination: true,
enableRowSelection: true,
state: {
sorting: view.sort
? [
{
id: view.sort.field,
desc: view.sort.direction === 'desc',
},
]
: [],
globalFilter: view.search,
pagination: {
pageIndex: view.page,
pageSize: view.perPage,
},
columnVisibility: columnVisibility ?? EMPTY_OBJECT,
},
onSortingChange: ( sortingUpdater ) => {
onChangeView( ( currentView ) => {
const sort =
typeof sortingUpdater === 'function'
? sortingUpdater(
currentView.sort
? [
{
id: currentView.sort.field,
desc:
currentView.sort
.direction === 'desc',
},
]
: []
)
: sortingUpdater;
if ( ! sort.length ) {
return {
...currentView,
sort: {},
};
}
const [ { id, desc } ] = sort;
return {
...currentView,
sort: { field: id, direction: desc ? 'desc' : 'asc' },
};
} );
},
onColumnVisibilityChange: ( columnVisibilityUpdater ) => {
onChangeView( ( currentView ) => {
const hiddenFields = Object.entries(
columnVisibilityUpdater()
).reduce(
( accumulator, [ fieldId, value ] ) => {
if ( value ) {
return accumulator.filter(
( id ) => id !== fieldId
);
}
return [ ...accumulator, fieldId ];
},
[ ...( currentView.hiddenFields || [] ) ]
);
return {
...currentView,
hiddenFields,
};
} );
},
onGlobalFilterChange: ( value ) => {
onChangeView( { ...view, search: value, page: 0 } );
},
onPaginationChange: ( paginationUpdater ) => {
onChangeView( ( currentView ) => {
const { pageIndex, pageSize } = paginationUpdater( {
pageIndex: currentView.page,
pageSize: currentView.perPage,
} );
return { ...view, page: pageIndex, perPage: pageSize };
} );
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
pageCount,
} );
const ViewComponent = view.type === 'list' ? ViewList : ViewGrid;
return (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously we need something more advanced than this to switch view types, but this should work for now. (Like a ViewType registry)

<div className="dataviews-wrapper">
<VStack spacing={ 4 }>
<HStack justify="space-between">
<TextFilter onChange={ dataView.setGlobalFilter } />
<TextFilter view={ view } onChangeView={ onChangeView } />
<ViewActions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this text filter independent of tanstack APIs.

fields={ fields }
view={ view }
onChangeView={ onChangeView }
/>
</HStack>
{ /* This component will be selected based on viewConfigs. Now we only have the list view. */ }
<ListView dataView={ dataView } isLoading={ isLoading } />
<ViewComponent
fields={ fields }
view={ view }
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
actions={ actions }
data={ data }
isLoading={ isLoading }
/>
<Pagination
dataView={ dataView }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this text filter independent of tanstack APIs.

totalItems={ paginationInfo?.totalItems }
view={ view }
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
/>
</VStack>
</div>
Expand Down
1 change: 0 additions & 1 deletion packages/edit-site/src/components/dataviews/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as DataViews } from './dataviews';
export { PAGE_SIZE_VALUES } from './view-actions';
91 changes: 64 additions & 27 deletions packages/edit-site/src/components/dataviews/pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,54 @@ import {
__experimentalHStack as HStack,
__experimentalText as Text,
__experimentalNumberControl as NumberControl,
__experimentalInputControlPrefixWrapper as InputControlPrefixWrapper,
SelectControl,
} from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { sprintf, __, _x, _n } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { PageSizeControl } from './view-actions';
const PAGE_SIZE_VALUES = [ 5, 20, 50 ];
function PageSizeControl( { view, onChangeView } ) {
const label = __( 'Rows per page:' );
return (
<SelectControl
__nextHasNoMarginBottom
label={ label }
hideLabelFromVision
// TODO: This should probably use a label based on the wanted design
// and we could remove InputControlPrefixWrapper usage.
prefix={
<InputControlPrefixWrapper
as="span"
className="dataviews__per-page-control-prefix"
>
{ label }
</InputControlPrefixWrapper>
}
value={ view.perPage }
options={ PAGE_SIZE_VALUES.map( ( pageSize ) => ( {
value: pageSize,
label: pageSize,
} ) ) }
onChange={ ( value ) =>
onChangeView( { ...view, perPage: value } )
}
/>
);
}

// For now this is copied from the patterns list Pagination component, because
// the datatable pagination starts from index zero(`0`). Eventually all lists will be
// using this one.
export function Pagination( {
dataView,
// If passed, use it, as it's for controlled pagination.
totalItems = 0,
function Pagination( {
view,
onChangeView,
paginationInfo: { totalItems = 0, totalPages },
} ) {
const currentPage = dataView.getState().pagination.pageIndex + 1;
const numPages = dataView.getPageCount();
const _totalItems = totalItems || dataView.getCoreRowModel().rows.length;
const currentPage = view.page + 1;
if ( ! totalItems || ! totalPages ) {
return null;
}
return (
<HStack
expanded={ false }
Expand All @@ -38,25 +66,27 @@ export function Pagination( {
// translators: %s: Total number of entries.
sprintf(
// translators: %s: Total number of entries.
_n( '%s item', '%s items', _totalItems ),
_totalItems
_n( '%s item', '%s items', totalItems ),
totalItems
)
}
</Text>
{ !! _totalItems && (
{ !! totalItems && (
<HStack expanded={ false } spacing={ 1 }>
<Button
variant="tertiary"
onClick={ () => dataView.setPageIndex( 0 ) }
disabled={ ! dataView.getCanPreviousPage() }
onClick={ () => onChangeView( { ...view, page: 0 } ) }
disabled={ view.page === 0 }
aria-label={ __( 'First page' ) }
>
«
</Button>
<Button
variant="tertiary"
onClick={ () => dataView.previousPage() }
disabled={ ! dataView.getCanPreviousPage() }
onClick={ () =>
onChangeView( { ...view, page: view.page - 1 } )
}
disabled={ view.page === 0 }
aria-label={ __( 'Previous page' ) }
>
Expand All @@ -71,17 +101,20 @@ export function Pagination( {
// translators: %1$s: Current page number, %2$s: Total number of pages.
_x( '<CurrenPageControl /> of %2$s', 'paging' ),
currentPage,
numPages
totalPages
),
{
CurrenPageControl: (
<NumberControl
aria-label={ __( 'Current page' ) }
min={ 1 }
max={ numPages }
max={ totalPages }
onChange={ ( value ) => {
if ( value > numPages ) return;
dataView.setPageIndex( value - 1 );
if ( value > totalPages ) return;
onChangeView( {
...view,
page: view.page - 1,
} );
} }
step="1"
value={ currentPage }
Expand All @@ -94,25 +127,29 @@ export function Pagination( {
</HStack>
<Button
variant="tertiary"
onClick={ () => dataView.nextPage() }
disabled={ ! dataView.getCanNextPage() }
onClick={ () =>
onChangeView( { ...view, page: view.page + 1 } )
}
disabled={ view.page >= totalPages - 1 }
aria-label={ __( 'Next page' ) }
>
</Button>
<Button
variant="tertiary"
onClick={ () =>
dataView.setPageIndex( dataView.getPageCount() - 1 )
onChangeView( { ...view, page: totalPages - 1 } )
}
disabled={ ! dataView.getCanNextPage() }
disabled={ view.page >= totalPages - 1 }
aria-label={ __( 'Last page' ) }
>
»
</Button>
</HStack>
) }
<PageSizeControl dataView={ dataView } />
<PageSizeControl view={ view } onChangeView={ onChangeView } />
</HStack>
);
}

export default Pagination;
Loading
Loading