diff --git a/package-lock.json b/package-lock.json index f5bc339dc15fef..3e61da1f729668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13942,6 +13942,37 @@ "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==" }, + "node_modules/@tanstack/react-table": { + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.3.tgz", + "integrity": "sha512-Ng9rdm3JPoSCi6cVZvANsYnF+UoGVRxflMb270tVj0+LjeT/ZtZ9ckxF6oLPLcKesza6VKBqtdF9mQ+vaz24Aw==", + "dependencies": { + "@tanstack/table-core": "8.9.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.3.tgz", + "integrity": "sha512-NpHZBoHTfqyJk0m/s/+CSuAiwtebhYK90mDuf5eylTvgViNOujiaOaxNDxJkQQAsVvHWZftUGAx1EfO1rkKtLg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", @@ -56752,6 +56783,7 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", + "@tanstack/react-table": "^8.9.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", @@ -67814,6 +67846,19 @@ "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==" }, + "@tanstack/react-table": { + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.3.tgz", + "integrity": "sha512-Ng9rdm3JPoSCi6cVZvANsYnF+UoGVRxflMb270tVj0+LjeT/ZtZ9ckxF6oLPLcKesza6VKBqtdF9mQ+vaz24Aw==", + "requires": { + "@tanstack/table-core": "8.9.3" + } + }, + "@tanstack/table-core": { + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.3.tgz", + "integrity": "sha512-NpHZBoHTfqyJk0m/s/+CSuAiwtebhYK90mDuf5eylTvgViNOujiaOaxNDxJkQQAsVvHWZftUGAx1EfO1rkKtLg==" + }, "@testing-library/dom": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", @@ -69876,6 +69921,7 @@ "version": "file:packages/edit-site", "requires": { "@babel/runtime": "^7.16.0", + "@tanstack/react-table": "^8.9.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 3441b79ff9dd4f..1c88ecdecec86b 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -27,6 +27,7 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.16.0", + "@tanstack/react-table": "^8.9.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", diff --git a/packages/edit-site/src/components/datatable/context.js b/packages/edit-site/src/components/datatable/context.js new file mode 100644 index 00000000000000..3e2aac8ca540c8 --- /dev/null +++ b/packages/edit-site/src/components/datatable/context.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +const DataTableContext = createContext( {} ); +export const useDataTableContext = () => useContext( DataTableContext ); +export default DataTableContext; diff --git a/packages/edit-site/src/components/datatable/datatable-actions.js b/packages/edit-site/src/components/datatable/datatable-actions.js new file mode 100644 index 00000000000000..07b15642d6261c --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-actions.js @@ -0,0 +1,180 @@ +/** + * WordPress dependencies + */ +import { + Button, + Icon, + SelectControl, + privateApis as componentsPrivateApis, + __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, +} from '@wordpress/components'; +import { + chevronRightSmall, + check, + blockTable, + chevronDown, +} from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { useDataTableContext } from './context'; + +const { + DropdownMenuV2, + // DropdownMenuCheckboxItemV2, + DropdownMenuGroupV2, + DropdownMenuItemV2, + // DropdownMenuRadioGroupV2, + // DropdownMenuRadioItemV2, + // DropdownMenuSeparatorV2, + DropdownSubMenuV2, + DropdownSubMenuTriggerV2, +} = unlock( componentsPrivateApis ); + +const PAGE_SIZE_VALUES = [ 2, 5, 20, 50 ]; + +export function DataTablePageSizeControl() { + const table = useDataTableContext(); + const prefix = __( 'Rows per page:' ); + return ( + + { prefix } + + } + value={ table.getState().pagination.pageSize } + options={ PAGE_SIZE_VALUES.map( ( pageSize ) => ( { + value: pageSize, + label: pageSize, + } ) ) } + onChange={ ( value ) => table.setPageSize( +value ) } + /> + ); +} + +// TODO: probably the selected value should be a user setting per list.. +function DataTablePageSizeMenu() { + const table = useDataTableContext(); + const currenPageSize = table.getState().pagination.pageSize; + return ( + + { currenPageSize } + { ' ' } + + } + > + { __( 'Rows per page' ) } + + } + > + { PAGE_SIZE_VALUES.map( ( size ) => { + return ( + + } + onSelect={ ( event ) => { + // We need to handle this on DropDown component probably.. + event.preventDefault(); + table.setPageSize( size ); + } } + // TODO: check about role and a11y. + role="menuitemcheckbox" + > + { size } + + ); + } ) } + + ); +} + +function DataTableColumnsVisibilityMenu() { + const table = useDataTableContext(); + const hideableColumns = table + .getAllColumns() + .filter( ( columnn ) => columnn.getCanHide() ); + if ( ! hideableColumns?.length ) { + return null; + } + return ( + } + > + { __( 'Columns' ) } + + } + > + { hideableColumns?.map( ( column ) => { + return ( + + } + onSelect={ ( event ) => { + event.preventDefault(); + column.getToggleVisibilityHandler()( event ); + } } + role="menuitemcheckbox" + > + { column.columnDef.header } + + ); + } ) } + + ); +} + +// function ResetControl( { onSelect } ) { +// return ( +// +// +// { __( 'Reset' ) } +// +// +// ); +// } + +export default function DataTableActions( { + className, + // TODO: check if we need something fixed here and use props + // or we would need slot and compose components.. + showColumnsVisibility = true, +} ) { + return ( + + { __( 'View' ) } + + + } + > + + { !! showColumnsVisibility && ( + + ) } + + + + ); +} diff --git a/packages/edit-site/src/components/datatable/datatable-bulk-actions.js b/packages/edit-site/src/components/datatable/datatable-bulk-actions.js new file mode 100644 index 00000000000000..64f2a0456e4c8c --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-bulk-actions.js @@ -0,0 +1,54 @@ +/** + * WordPress dependencies + */ +import { + Button, + Popover, + __experimentalHStack as HStack, + __experimentalText as Text, +} from '@wordpress/components'; +import { sprintf, __, _n } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useDataTableContext } from './context'; + +export default function DataTableBulkActions( { anchor, children } ) { + const table = useDataTableContext(); + // TODO: probably will need to reset the selection on various actions or just in `data` change.. + const selectedRows = table.getSelectedRowModel().flatRows; + // TODO: if not children and fills, do not render.. + // const fills = useSlotFills( DATATABLE_BULK_ACTIONS_SLOT_NAME ); + // if ( ! selectedRows.length || ! fills?.length ) { + // return null; + // } + if ( ! selectedRows.length ) { + return null; + } + return ( + + + + { + // translators: %s: Total number of selected entries. + sprintf( + // translators: %s: Total number of selected entries. + _n( '%s item', '%s items', selectedRows.length ), + selectedRows.length + ) + } + + { children } + + + + ); +} diff --git a/packages/edit-site/src/components/datatable/datatable-global-search-input.js b/packages/edit-site/src/components/datatable/datatable-global-search-input.js new file mode 100644 index 00000000000000..418841ee31b02f --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-global-search-input.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import DataTableTextFilter from './datatable-text-filter'; +import { useDataTableContext } from './context'; + +// type DataTableTextFilterProps = { +// className: string; +// searchLabel: string; +// onChange: any; +// }; + +export default function DataTableGlobalSearchInput( props ) { + const table = useDataTableContext(); + return ( + + ); +} diff --git a/packages/edit-site/src/components/datatable/datatable-pagination.js b/packages/edit-site/src/components/datatable/datatable-pagination.js new file mode 100644 index 00000000000000..5674b9cee4c6ab --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-pagination.js @@ -0,0 +1,180 @@ +/** + * WordPress dependencies + */ +import { + Button, + __experimentalHStack as HStack, + __experimentalText as Text, + __experimentalNumberControl as NumberControl, +} from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; +import { sprintf, __, _x, _n } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useDataTableContext } from './context'; +import { DataTablePageSizeControl } from './datatable-actions'; + +export function DataTablePaginationTotalItems( { + // If passed, use it as it's for controlled pagination. + totalItems = 0, +} ) { + const table = useDataTableContext(); + return ( + + { sprintf( + // translators: %s: Total number of items id lists. + __( '%s items' ), + totalItems || table.getCoreRowModel().rows.length + ) } + + ); +} + +//This function implements a pagination style similar to WP core's `paginate_links`. +// It's not used currently, but was suggested in some of the designs. +export function DataTablePaginationNumbers() { + const table = useDataTableContext(); + const totalPages = table.getPageCount(); + if ( ! totalPages ) { + return null; + } + const currentPage = table.getState().pagination.pageIndex + 1; + const pageLinks = []; + const midSize = 2; + const endSize = 1; + let dots = false; + for ( let i = 1; i <= totalPages; i++ ) { + const isActive = i === currentPage; + if ( isActive ) { + pageLinks.push( + + + + { createInterpolateElement( + sprintf( + // translators: %1$s: Current page number, %2$s: Total number of pages. + _x( ' of %2$s', 'paging' ), + currentPage, + numPages + ), + { + CurrenPageControl: ( + { + if ( value > numPages ) return; + table.setPageIndex( value - 1 ); + } } + step="1" + value={ currentPage } + isDragEnabled={ false } + spinControls="none" + /> + ), + } + ) } + + + + + + + ); +} diff --git a/packages/edit-site/src/components/datatable/datatable-provider.js b/packages/edit-site/src/components/datatable/datatable-provider.js new file mode 100644 index 00000000000000..553ebc1f4205fa --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-provider.js @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + useReactTable, +} from '@tanstack/react-table'; + +/** + * WordPress dependencies + */ +import { CheckboxControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ + +import DataTableContext from './context'; + +// type DataTableProps = { +// data: any, +// columns: any, +// options: any, +// children: any, +// }; + +export default function DataTable( { data, columns, options = {}, children } ) { + let _columns = columns; + if ( options.enableRowSelection ) { + _columns = [ + { + id: 'select', + header: ( { table } ) => { + const areAllRowsSelected = table.getIsAllPageRowsSelected(); + const canMultiSelect = table + .getRowModel() + .rows.some( ( row ) => row.getCanSelect() ); + return ( + canMultiSelect && ( + + table.toggleAllPageRowsSelected( !! value ) + } + aria-label={ __( 'Select all' ) } + __nextHasNoMarginBottom + /> + ) + ); + }, + cell: ( { row } ) => + row.getCanSelect() && ( + + row.toggleSelected( !! value ) + } + aria-label={ __( 'Select row' ) } + __nextHasNoMarginBottom + /> + ), + enableSorting: false, + enableHiding: false, + width: 35, + maxWidth: 35, + }, + ...columns, + ]; + } + const table = useReactTable( { + data, + columns: _columns, + ...options, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + } ); + return ( + + { children } + + ); +} diff --git a/packages/edit-site/src/components/datatable/datatable-rows.js b/packages/edit-site/src/components/datatable/datatable-rows.js new file mode 100644 index 00000000000000..862880f8bb076d --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-rows.js @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { flexRender } from '@tanstack/react-table'; + +/** + * WordPress dependencies + */ +import { Icon, chevronDown, chevronUp } from '@wordpress/icons'; +import { __experimentalHStack as HStack } from '@wordpress/components'; +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useDataTableContext } from './context'; + +// type DataTableRowsProps = { +// className?: string; +// isLoading: boolean; +// }; + +function SortingIcon( { header } ) { + const sortDirection = header.column.getIsSorted(); + if ( ! header.column.getCanSort() || ! sortDirection ) { + return null; + } + return ; +} + +function DataTableRows( + { className = 'datatable-component__table', isLoading = false }, + ref +) { + const table = useDataTableContext(); + const { rows } = table.getRowModel(); + const hasRows = !! rows?.length; + if ( isLoading ) { + // Add spinner or progress bar.. + return

Loading now..

; + } + return ( + <> + { hasRows && ( + + + { table.getHeaderGroups().map( ( headerGroup ) => ( + + { headerGroup.headers.map( ( header ) => ( + + ) ) } + + ) ) } + + + { rows.map( ( row ) => ( + + { row.getVisibleCells().map( ( cell ) => ( + + ) ) } + + ) ) } + +
+ { header.isPlaceholder ? null : ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + + + { flexRender( + header.column.columnDef + .header, + header.getContext() + ) } + + + + ) } +
+ { flexRender( + cell.column.columnDef.cell, + cell.getContext() + ) } +
+ ) } + { ! hasRows &&

no results

} + + ); +} + +export default forwardRef( DataTableRows ); diff --git a/packages/edit-site/src/components/datatable/datatable-text-filter.js b/packages/edit-site/src/components/datatable/datatable-text-filter.js new file mode 100644 index 00000000000000..23002d1b783b84 --- /dev/null +++ b/packages/edit-site/src/components/datatable/datatable-text-filter.js @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; +import { SearchControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useDebouncedInput from '../../utils/use-debounced-input'; + +// export type DataTableTextFilterProps = { +// className: string; +// searchLabel: string; +// onChange: any; +// }; + +export default function DataTableTextFilter( { + className, + searchLabel = __( 'Search' ), + onChange, +} ) { + const [ search, setSearch, debouncedSearch ] = useDebouncedInput(); + const baseCssClass = 'some-class-name'; + useEffect( () => { + onChange( debouncedSearch ); + }, [ debouncedSearch, onChange ] ); + return ( + + ); +} diff --git a/packages/edit-site/src/components/datatable/index.js b/packages/edit-site/src/components/datatable/index.js new file mode 100644 index 00000000000000..7a8a7afe0b8e1c --- /dev/null +++ b/packages/edit-site/src/components/datatable/index.js @@ -0,0 +1,12 @@ +export { default as DataTableProvider } from './datatable-provider'; +export { default as DataTableTextFilter } from './datatable-text-filter'; +export { default as DataTableGlobalSearchInput } from './datatable-global-search-input'; +export { default as DataTableRows } from './datatable-rows'; +export { default as DataTableActions } from './datatable-actions'; +export { default as DataTableBulkActions } from './datatable-bulk-actions'; +export { + DataTablePaginationTotalItems, + DataTablePaginationNumbers, + DataTablePagination, +} from './datatable-pagination'; +export { useDataTableContext } from './context'; diff --git a/packages/edit-site/src/components/datatable/style.scss b/packages/edit-site/src/components/datatable/style.scss new file mode 100644 index 00000000000000..063c7cbfcc7050 --- /dev/null +++ b/packages/edit-site/src/components/datatable/style.scss @@ -0,0 +1,11 @@ +.datatable-bulk-actions__popover { + .components-popover__content { + width: auto; + padding: $grid-unit-10; + } +} + +.edit-site-table__per-page-control-prefix { + color: $gray-700; + text-wrap: nowrap; +} diff --git a/packages/edit-site/src/components/list/added-by.js b/packages/edit-site/src/components/list/added-by.js index da6111e0b29bd5..3bef4e90f64d1b 100644 --- a/packages/edit-site/src/components/list/added-by.js +++ b/packages/edit-site/src/components/list/added-by.js @@ -1,4 +1,3 @@ -// @ts-check /** * External dependencies */ @@ -7,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Icon, __experimentalHStack as HStack } from '@wordpress/components'; +import { __experimentalHStack as HStack } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; @@ -30,7 +29,7 @@ import { /** @typedef {'wp_template'|'wp_template_part'} TemplateType */ -/** @type {TemplateType} */ +/** @type {TemplateType[]} */ const TEMPLATE_POST_TYPE_NAMES = [ TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE, @@ -172,27 +171,23 @@ function AvatarImage( { imageUrl } ) { /** * @param {Object} props - * @param {TemplateType} props.postType The template post type. - * @param {number} props.postId The template post id. + * @param {TemplateType} props.postType The template post type. + * @param {number} props.postId The template post id. + * @param {boolean} props.showIsCustomizedInfo */ -export default function AddedBy( { postType, postId } ) { - const { text, icon, imageUrl, isCustomized } = useAddedBy( - postType, - postId - ); +export default function AddedBy( { + postType, + postId, + showIsCustomizedInfo = true, +} ) { + const { text, imageUrl, isCustomized } = useAddedBy( postType, postId ); return ( - { imageUrl ? ( - - ) : ( -
- -
- ) } + { !! imageUrl && } { text } - { isCustomized && ( + { showIsCustomizedInfo && isCustomized && ( { postType === TEMPLATE_POST_TYPE ? _x( 'Customized', 'template' ) diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index 0979b7ac7e3a6a..e14cb8602adbd3 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -165,12 +165,12 @@ overflow: hidden; border-radius: 100%; background: $gray-800; - width: $grid-unit-40; - height: $grid-unit-40; + width: $grid-unit-30; + height: $grid-unit-30; img { - width: $grid-unit-40; - height: $grid-unit-40; + width: $grid-unit-30; + height: $grid-unit-30; object-fit: cover; opacity: 0; transition: opacity 0.1s linear; diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js index af017a8db9700a..1d9b8eedaabc5b 100644 --- a/packages/edit-site/src/components/page-main/index.js +++ b/packages/edit-site/src/components/page-main/index.js @@ -9,6 +9,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import PagePatterns from '../page-patterns'; import PageTemplateParts from '../page-template-parts'; import PageTemplates from '../page-templates'; +import PagePages from '../page-pages'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); @@ -24,6 +25,8 @@ export default function PageMain() { return ; } else if ( path === '/patterns' ) { return ; + } else if ( path === '/pages' ) { + return ; } return null; diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js new file mode 100644 index 00000000000000..760537915a0941 --- /dev/null +++ b/packages/edit-site/src/components/page-pages/index.js @@ -0,0 +1,234 @@ +/** + * WordPress dependencies + */ +import { applyFilters } from '@wordpress/hooks'; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +import { + VisuallyHidden, + __experimentalHeading as Heading, + __experimentalVStack as VStack, + __experimentalHStack as HStack, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useEntityRecords } from '@wordpress/core-data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useState, useEffect, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import PagesBulkActions from './pages-bulk-actions'; +import Page from '../page'; +import Link from '../routes/link'; +import PageActions from '../page-actions'; +import { + DataTableRows, + DataTableGlobalSearchInput, + DataTablePagination, + DataTableProvider, + DataTableActions, +} from '../datatable'; + +const EMPTY_ARRAY = []; + +function ToggleStatusFilter( { onChange } ) { + return ( + { + onChange( ! value ? defaultStatus : value ); + } } + > + + + + + ); +} + +const defaultStatus = [ 'publish', 'draft' ]; + +export default function PagePages() { + const [ bulkActionsAnchor, setBulkActionsAnchor ] = useState(); + const [ reset, setResetQuery ] = useState( ( v ) => ! v ); + const [ globalFilter, setGlobalFilter ] = useState( '' ); + // const [ rowSelection, setRowSelection ] = useState( {} ); + const [ status, setStatus ] = useState( defaultStatus ); + const [ paginationInfo, setPaginationInfo ] = useState(); + const [ { pageIndex, pageSize }, setPagination ] = useState( { + pageIndex: 0, + pageSize: 2, + } ); + // TODO: probably memo other objects passed as state(ex:https://tanstack.com/table/v8/docs/examples/react/pagination-controlled). + const pagination = useMemo( + () => ( { pageIndex, pageSize } ), + [ pageIndex, pageSize ] + ); + const [ sorting, setSorting ] = useState( [ + { order: 'desc', orderby: 'date' }, + ] ); + const queryArgs = useMemo( + () => ( { + per_page: pageSize, + page: pageIndex + 1, // tanstack starts from zero. + _embed: 'author', + order: sorting[ 0 ]?.desc ? 'desc' : 'asc', + orderby: sorting[ 0 ]?.id, + search: globalFilter, + status, + } ), + [ + globalFilter, + sorting[ 0 ]?.id, + sorting[ 0 ]?.desc, + pageSize, + pageIndex, + status, + reset, + ] + ); + const { records, isResolving: isLoading } = useEntityRecords( + 'postType', + 'page', + queryArgs + ); + useEffect( () => { + // Make extra request to handle controlled pagination. + apiFetch( { + path: addQueryArgs( '/wp/v2/pages', { + ...queryArgs, + _fields: 'id', + } ), + method: 'HEAD', + parse: false, + } ).then( ( res ) => { + const totalPages = parseInt( res.headers.get( 'X-WP-TotalPages' ) ); + const totalItems = parseInt( res.headers.get( 'X-WP-Total' ) ); + setPaginationInfo( { + totalPages, + totalItems, + } ); + } ); + // Status should not make extra request if already did.. + }, [ globalFilter, pageSize, status.toString(), reset ] ); + + const columns = useMemo( + () => [ + { + header: __( 'Title' ), + id: 'title', + accessorFn: ( page ) => page.title?.rendered || page.slug, + cell: ( props ) => { + const page = props.row.original; + return ( + + + + { decodeEntities( props.getValue() ) } + + + + ); + }, + maxWidth: 400, + sortingFn: 'alphanumeric', + enableHiding: false, + }, + { + header: __( 'Author' ), + id: 'author', + accessorFn: ( page ) => page._embedded?.author[ 0 ]?.name, + cell: ( props ) => { + const author = props.row.original._embedded?.author[ 0 ]; + // TODO: change to wp-admin link. + return { author.name }; + }, + }, + { + header: 'Status', + id: 'status', + cell: ( props ) => props.row.original.status, + }, + { + header: { __( 'Actions' ) }, + id: 'actions', + cell: ( props ) => { + const page = props.row.original; + return ( + setResetQuery() } + /> + ); + }, + enableHiding: false, + }, + ], + [] + ); + + const columnsWithFilters = applyFilters( + 'siteEditor.pageListColumns', + columns + ); + + // TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`. + return ( + +
+ + + + + + + + + + + + +
+
+ ); +} diff --git a/packages/edit-site/src/components/page-pages/pages-bulk-actions-slot.js b/packages/edit-site/src/components/page-pages/pages-bulk-actions-slot.js new file mode 100644 index 00000000000000..ce24a635e03276 --- /dev/null +++ b/packages/edit-site/src/components/page-pages/pages-bulk-actions-slot.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { + __experimentalUseSlotFills as useSlotFills, + createSlotFill, +} from '@wordpress/components'; + +const slotName = 'pages.list.bulkActions'; + +const { Fill, Slot: PagesListBulkActionsSlot } = createSlotFill( slotName ); + +const PagesListBulkActions = Fill; + +const Slot = ( { children } ) => { + const fills = useSlotFills( slotName ); + // TODO: pass the context to children or just let the consumers + // just use `useDataTableContext`. Probably let the consumers.. + // const table = useDataTableContext(); + if ( ! fills?.length ) { + return children; + } + return ; +}; +PagesListBulkActions.Slot = Slot; + +export default PagesListBulkActions; diff --git a/packages/edit-site/src/components/page-pages/pages-bulk-actions.js b/packages/edit-site/src/components/page-pages/pages-bulk-actions.js new file mode 100644 index 00000000000000..1cb1ee5460cd9c --- /dev/null +++ b/packages/edit-site/src/components/page-pages/pages-bulk-actions.js @@ -0,0 +1,88 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { + Button, + __experimentalConfirmDialog as ConfirmDialog, +} from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { DataTableBulkActions, useDataTableContext } from '../datatable'; +import PagesListBulkActions from './pages-bulk-actions-slot'; + +export default function PagesBulkActions( { anchor } ) { + // Extenders can add extra bulk actions through `registerPlugin`. + return ( + + + + + ); +} + +function DeleteMenuItem( { onRemove } ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const table = useDataTableContext(); + const { createSuccessNotice } = useDispatch( noticesStore ); + const { deleteEntityRecord } = useDispatch( coreStore ); + + async function deletePages( items ) { + const deletePromises = items.map( ( { original: { id, type } } ) => + deleteEntityRecord( + 'postType', + type, + id, + {}, + { throwOnError: true } + ) + ); + const deleteRequests = await Promise.allSettled( deletePromises ); + if ( + deleteRequests.every( ( { status } ) => status === 'fulfilled' ) + ) { + createSuccessNotice( __( 'Selected pages successfully removed' ), { + type: 'snackbar', + id: 'pages-list-bulk-delete-success', + } ); + } + // At least a deletion has failed, so accumulate the results and display an + // appropriate message. We'll probably need a custom promise that also has + // the titles of entities that failed. + // deleteRequests.forEach( ( ob ) => { + // console.log( ob ); + // } ); + } + return ( + <> + + { + await deletePages( table.getSelectedRowModel().flatRows ); + setIsModalOpen( false ); + // TODO: check for better handling.. Stil not working on all cases(ex paging). + table.options.meta.resetQuery(); // This can reset the query. + table.reset(); // This resets the table's state. + onRemove?.(); + } } + onCancel={ () => setIsModalOpen( false ) } + confirmButtonText={ __( 'Delete' ) } + > + { __( 'Are you sure you want to delete the selected items?' ) } + + + ); +} diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 55c666970b5cc2..8f6d16d463fde0 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -4,10 +4,12 @@ import { VisuallyHidden, __experimentalHeading as Heading, - __experimentalText as Text, __experimentalVStack as VStack, + __experimentalHStack as HStack, + __experimentalText as Text, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; @@ -15,14 +17,56 @@ import { decodeEntities } from '@wordpress/html-entities'; * Internal dependencies */ import Page from '../page'; -import Table from '../table'; import Link from '../routes/link'; -import AddedBy from '../list/added-by'; +import { default as AddedBy, useAddedBy } from '../list/added-by'; import TemplateActions from '../template-actions'; import AddNewTemplate from '../add-new-template'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import isTemplateRemovable from '../../utils/is-template-removable'; +import isTemplateRevertable from '../../utils/is-template-revertable'; +import { + DataTableRows, + DataTableGlobalSearchInput, + DataTablePagination, + DataTableProvider, + DataTableActions, +} from '../datatable'; +import TemplatesBulkActions from './templates-bulk-actions'; + +function TemplateTitle( props ) { + const template = props.row.original; + const { isCustomized } = useAddedBy( template.type, template.id ); + return ( + + + + { decodeEntities( props.getValue() ) } + + + { isCustomized && ( + + { template.type === 'wp_template' + ? _x( 'Customized', 'template' ) + : _x( 'Customized', 'template part' ) } + + ) } + + ); +} export default function PageTemplates() { + const [ bulkActionsAnchor, setBulkActionsAnchor ] = useState(); const { records: templates } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, @@ -30,50 +74,52 @@ export default function PageTemplates() { per_page: -1, } ); - - 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 columns = useMemo( + () => [ + { + header: __( 'Template' ), + id: 'title', + accessorFn: ( template ) => + template.title?.rendered || template.slug, + cell: ( props ) => { + return ; + }, + maxSize: 400, + sortingFn: 'alphanumeric', + enableHiding: false, + }, + { + header: __( 'Added by' ), + id: 'addedBy', + cell: ( props ) => { + const template = props.row.original; + return ( + + ); + }, + }, + { + header: { __( 'Actions' ) }, + id: 'actions', + cell: ( props ) => { + const template = props.row.original; + return ( + + ); + }, + enableHiding: false, + }, + ], + [] + ); return ( } > - { templates && } + { templates && ( +
+ + isTemplateRemovable( template ) || + isTemplateRevertable( template ), + } } + > + + + + + + + + + + +
+ ) } ); } diff --git a/packages/edit-site/src/components/page-templates/templates-bulk-actions.js b/packages/edit-site/src/components/page-templates/templates-bulk-actions.js new file mode 100644 index 00000000000000..0a4b86b8645d73 --- /dev/null +++ b/packages/edit-site/src/components/page-templates/templates-bulk-actions.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { DataTableBulkActions } from '../datatable'; + +export default function TemplatesBulkActions( { anchor } ) { + // What happens if one of the selected rows can be deleted and the other one can be reset? + return ( + + + + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages-list/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages-list/index.js new file mode 100644 index 00000000000000..4337725efbb677 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages-list/index.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import SidebarNavigationScreen from '../sidebar-navigation-screen'; + +export default function SidebarNavigationScreenPagesList() { + return ( + + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js index 4d143235e9597e..38fc216d2a85df 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js @@ -220,10 +220,13 @@ export default function SidebarNavigationScreenPages() { ) ) } { - document.location = 'edit.php?post_type=page'; - } } + { ...useLink( { + path: '/pages', + } ) } + // href="edit.php?post_type=page" + // onClick={ () => { + // document.location = 'edit.php?post_type=page'; + // } } > { __( 'Manage all pages' ) } diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 9e035759ea9ad6..670f53ec2a6e4d 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -27,6 +27,7 @@ import SaveHub from '../save-hub'; import { unlock } from '../../lock-unlock'; import SidebarNavigationScreenPages from '../sidebar-navigation-screen-pages'; import SidebarNavigationScreenPage from '../sidebar-navigation-screen-page'; +import SidebarNavigationScreenPagesList from '../sidebar-navigation-screen-pages-list'; const { useLocation } = unlock( routerPrivateApis ); @@ -53,6 +54,9 @@ function SidebarScreens() { + + + diff --git a/packages/edit-site/src/components/table/style.scss b/packages/edit-site/src/components/table/style.scss index 85c741575d5a44..e006644ffa4a5d 100644 --- a/packages/edit-site/src/components/table/style.scss +++ b/packages/edit-site/src/components/table/style.scss @@ -18,17 +18,11 @@ padding: 0 $grid-unit-20 $grid-unit-20; color: $gray-700; } - td { - padding: $grid-unit-20; - } td, th { - vertical-align: center; - &:first-child { - padding-left: 0; - } + padding: $grid-unit-15; + &:last-child { - padding-right: 0; text-align: right; } } diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 18cb0d5e5db69b..6680f2519122e7 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -109,4 +109,5 @@ export { default as PluginSidebar } from './components/sidebar-edit-mode/plugin- export { default as PluginSidebarMoreMenuItem } from './components/header-edit-mode/plugin-sidebar-more-menu-item'; export { default as PluginMoreMenuItem } from './components/header-edit-mode/plugin-more-menu-item'; export { default as PluginTemplateSettingPanel } from './components/plugin-template-setting-panel'; +export { default as PagesListBulkActions } from './components/page-pages/pages-bulk-actions-slot'; export { store } from './store'; diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 111696241d0d69..e8a5036360d07d 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -4,6 +4,7 @@ @import "./components/block-editor/style.scss"; @import "./components/canvas-loader/style.scss"; @import "./components/code-editor/style.scss"; +@import "./components/datatable/style.scss"; @import "./components/global-styles/style.scss"; @import "./components/global-styles/screen-revisions/style.scss"; @import "./components/header-edit-mode/style.scss"; diff --git a/packages/edit-site/src/utils/get-is-list-page.js b/packages/edit-site/src/utils/get-is-list-page.js index 600e686618bf94..2ee661253cf063 100644 --- a/packages/edit-site/src/utils/get-is-list-page.js +++ b/packages/edit-site/src/utils/get-is-list-page.js @@ -14,8 +14,9 @@ export default function getIsListPage( isMobileViewport ) { return ( - path === '/wp_template/all' || - path === '/wp_template_part/all' || + [ '/wp_template/all', '/wp_template_part/all', '/pages' ].includes( + path + ) || ( path === '/patterns' && // Don't treat "/patterns" without categoryType and categoryId as a // list page in mobile because the sidebar covers the whole page.