diff --git a/.eslintrc.js b/.eslintrc.js index 0fc37713dce4d..97c9f395194f3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -295,6 +295,7 @@ module.exports = { 'FocalPointPicker', 'RangeControl', 'SearchControl', + 'TextControl', 'TextareaControl', 'ToggleGroupControl', 'TreeSelect', diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index a2bcf31b4d2e2..f4138c49dc8d6 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -2,7 +2,106 @@ Namespace: `core`. -## Selectors +### Dynamically generated selectors + +There are a number of user-friendly selectors that are wrappers of the more generic `getEntityRecord` and `getEntityRecords` that can be used to retrieve information for the various entities. + +### getPostType + +Returns the information for a given post type. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const postType = useSelect( + ( select ) => select( coreDataStore ).getPostType( 'post' ) + + // Equivalent to: select( coreDataStore ).getEntityRecord( 'root', 'postType', 'post' ) + ); + +_Parameters_ + +- postType `string` + +_Returns_ + +- `EntityRecord | undefined`: Record. + +### getPostTypes + +Returns the information for post types. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const postTypes = useSelect( ( select ) => { + return select( coreDataStore ).getPostTypes( { per_page: 4 } ); + + // Equivalent to: + // select( coreDataStore ).getEntityRecords( 'root', 'postType', { per_page: 4 } ); + } ); + +_Parameters_ + +- _query_ `GetRecordsHttpQuery`: Optional terms query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available for "List [Entity kind]s". + +_Returns_ + +- `EntityRecord[] | null`: Records. + +### getTaxonomy + +Returns information for a given taxonomy. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const taxonomy = useSelect( ( select ) => { + return select( coreDataStore ).getTaxonomy( 'category' ); + + // Equivalent to: + // select( coreDataStore ).getEntityRecord( 'root', 'taxonomy', 'category' ); + } ); + +_Parameters_ + +- taxonomy `string` + +_Returns_ + +- `EntityRecord | undefined`: Record. + +### getTaxonomies + +Returns information for taxonomies. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const taxonomies = useSelect( ( select ) => { + return select( coreDataStore ).getTaxonomies( { type: 'post' } ); + + // Equivalent to: + // select( coreDataStore ).getEntityRecords( 'root', 'taxonomy', { type: 'post' } ); + } ); + +_Parameters_ + +- _query_ `GetRecordsHttpQuery`: Optional terms query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available for "List [Entity kind]s". + +_Returns_ + +- `EntityRecord[] | null`: Records. + +## Other Selectors diff --git a/lib/experimental/class-wp-rest-customizer-nonces.php b/lib/experimental/class-wp-rest-customizer-nonces.php deleted file mode 100644 index f4202a672f520..0000000000000 --- a/lib/experimental/class-wp-rest-customizer-nonces.php +++ /dev/null @@ -1,74 +0,0 @@ -namespace = '__experimental'; - $this->rest_base = 'customizer-nonces'; - } - - /** - * Registers the necessary REST API routes. - * - * @access public - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/get-save-nonce', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_save_nonce' ), - 'permission_callback' => array( $this, 'permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Checks if a given request has access to read menu items if they have access to edit them. - * - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - */ - public function permissions_check() { - $post_type = get_post_type_object( 'nav_menu_item' ); - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Returns the nonce required to request the customizer API endpoint. - * - * @access public - */ - public function get_save_nonce() { - require_once ABSPATH . 'wp-includes/class-wp-customize-manager.php'; - $wp_customize = new WP_Customize_Manager(); - $nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ); - return array( - 'success' => true, - 'nonce' => $nonce, - 'stylesheet' => $wp_customize->get_stylesheet(), - ); - } - } -} diff --git a/lib/experimental/rest-api.php b/lib/experimental/rest-api.php index 77f7d091d2655..6bb2947f88914 100644 --- a/lib/experimental/rest-api.php +++ b/lib/experimental/rest-api.php @@ -10,15 +10,6 @@ die( 'Silence is golden.' ); } -/** - * Registers the customizer nonces REST API routes. - */ -function gutenberg_register_rest_customizer_nonces() { - $customizer_nonces = new WP_Rest_Customizer_Nonces(); - $customizer_nonces->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_customizer_nonces' ); - /** * Registers the Block editor settings REST API routes. */ diff --git a/lib/load.php b/lib/load.php index 244b6a2d9e685..5a299f3b69696 100644 --- a/lib/load.php +++ b/lib/load.php @@ -45,12 +45,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php'; require_once __DIR__ . '/rest-api.php'; - // Experimental. - if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { - require_once __DIR__ . '/experimental/class-wp-rest-customizer-nonces.php'; - } require_once __DIR__ . '/experimental/rest-api.php'; - require_once __DIR__ . '/experimental/kses-allowed-html.php'; } diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index b8a9088cfd72c..4d5f22e02fa7d 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -208,6 +208,9 @@ $z-layers: ( // Ensure checkbox + actions don't overlap table header ".dataviews-view-table thead": 1, + // Ensure selection checkbox stays above the preview field. + ".dataviews-view-grid__card .dataviews-selection-checkbox": 1, + // Ensure quick actions toolbar appear above pagination ".dataviews-bulk-actions-toolbar": 2, ); diff --git a/packages/block-editor/src/components/block-mover/index.js b/packages/block-editor/src/components/block-mover/index.js index 3b7f25800095a..7aa273605bbe6 100644 --- a/packages/block-editor/src/components/block-mover/index.js +++ b/packages/block-editor/src/components/block-mover/index.js @@ -89,7 +89,6 @@ function BlockMover( { - } - > + { inactiveFilters.map( ( filter ) => { return ( +) { + if ( ! filters.length || filters.every( ( { isPrimary } ) => isPrimary ) ) { + return null; + } + const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible ); + return ( + + { __( 'Add filter' ) } + + } + { ...{ filters, view, onChangeView, setOpenedFilter } } + /> + ); +} + export default forwardRef( AddFilter ); diff --git a/packages/dataviews/src/components/dataviews-filters/index.tsx b/packages/dataviews/src/components/dataviews-filters/index.tsx index 449e0ff0323a8..de3477914c008 100644 --- a/packages/dataviews/src/components/dataviews-filters/index.tsx +++ b/packages/dataviews/src/components/dataviews-filters/index.tsx @@ -1,65 +1,150 @@ /** * WordPress dependencies */ -import { memo, useContext, useRef } from '@wordpress/element'; -import { __experimentalHStack as HStack } from '@wordpress/components'; +import { + memo, + useContext, + useRef, + useMemo, + useCallback, +} from '@wordpress/element'; +import { __experimentalHStack as HStack, Button } from '@wordpress/components'; +import { funnel } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import FilterSummary from './filter-summary'; -import AddFilter from './add-filter'; +import { default as AddFilter, AddFilterDropdownMenu } from './add-filter'; import ResetFilters from './reset-filters'; import DataViewsContext from '../dataviews-context'; import { sanitizeOperators } from '../../utils'; import { ALL_OPERATORS, OPERATOR_IS, OPERATOR_IS_NOT } from '../../constants'; -import type { NormalizedFilter } from '../../types'; +import type { NormalizedFilter, NormalizedField, View } from '../../types'; -function Filters() { - const { fields, view, onChangeView, openedFilter, setOpenedFilter } = - useContext( DataViewsContext ); - const addFilterRef = useRef< HTMLButtonElement >( null ); - const filters: NormalizedFilter[] = []; - fields.forEach( ( field ) => { - if ( ! field.elements?.length ) { - return; - } +export function useFilters( fields: NormalizedField< any >[], view: View ) { + return useMemo( () => { + const filters: NormalizedFilter[] = []; + fields.forEach( ( field ) => { + if ( ! field.elements?.length ) { + return; + } - const operators = sanitizeOperators( field ); - if ( operators.length === 0 ) { - return; - } + const operators = sanitizeOperators( field ); + if ( operators.length === 0 ) { + return; + } - const isPrimary = !! field.filterBy?.isPrimary; - filters.push( { - field: field.id, - name: field.label, - elements: field.elements, - singleSelection: operators.some( ( op ) => - [ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op ) - ), - operators, - isVisible: - isPrimary || - !! view.filters?.some( - ( f ) => - f.field === field.id && - ALL_OPERATORS.includes( f.operator ) + const isPrimary = !! field.filterBy?.isPrimary; + filters.push( { + field: field.id, + name: field.label, + elements: field.elements, + singleSelection: operators.some( ( op ) => + [ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op ) ), - isPrimary, + operators, + isVisible: + isPrimary || + !! view.filters?.some( + ( f ) => + f.field === field.id && + ALL_OPERATORS.includes( f.operator ) + ), + isPrimary, + } ); } ); - } ); - // Sort filters by primary property. We need the primary filters to be first. - // Then we sort by name. - filters.sort( ( a, b ) => { - if ( a.isPrimary && ! b.isPrimary ) { - return -1; - } - if ( ! a.isPrimary && b.isPrimary ) { - return 1; - } - return a.name.localeCompare( b.name ); - } ); + // Sort filters by primary property. We need the primary filters to be first. + // Then we sort by name. + filters.sort( ( a, b ) => { + if ( a.isPrimary && ! b.isPrimary ) { + return -1; + } + if ( ! a.isPrimary && b.isPrimary ) { + return 1; + } + return a.name.localeCompare( b.name ); + } ); + return filters; + }, [ fields, view ] ); +} + +export function FilterVisibilityToggle( { + filters, + view, + onChangeView, + setOpenedFilter, + isShowingFilter, + setIsShowingFilter, +}: { + filters: NormalizedFilter[]; + view: View; + onChangeView: ( view: View ) => void; + setOpenedFilter: ( filter: string | null ) => void; + isShowingFilter: boolean; + setIsShowingFilter: React.Dispatch< React.SetStateAction< boolean > >; +} ) { + const onChangeViewWithFilterVisibility = useCallback( + ( _view: View ) => { + onChangeView( _view ); + setIsShowingFilter( true ); + }, + [ onChangeView, setIsShowingFilter ] + ); + const visibleFilters = filters.filter( ( filter ) => filter.isVisible ); + + const hasVisibleFilters = !! visibleFilters.length; + if ( ! hasVisibleFilters ) { + return ( + + } + /> + ); + } + return ( +
+
+ ); +} + +function Filters() { + const { fields, view, onChangeView, openedFilter, setOpenedFilter } = + useContext( DataViewsContext ); + const addFilterRef = useRef< HTMLButtonElement >( null ); + const filters = useFilters( fields, view ); const addFilter = ( ); + const visibleFilters = filters.filter( ( filter ) => filter.isVisible ); + if ( visibleFilters.length === 0 ) { + return null; + } const filterComponents = [ - ...filters.map( ( filter ) => { - if ( ! filter.isVisible ) { - return null; - } - + ...visibleFilters.map( ( filter ) => { return ( 1 ) { - filterComponents.push( - - ); - } + filterComponents.push( + + ); return ( - + { filterComponents } ); diff --git a/packages/dataviews/src/components/dataviews-filters/style.scss b/packages/dataviews/src/components/dataviews-filters/style.scss index 26e5e613fcbe4..6912b5cc48316 100644 --- a/packages/dataviews/src/components/dataviews-filters/style.scss +++ b/packages/dataviews/src/components/dataviews-filters/style.scss @@ -2,6 +2,10 @@ position: relative; } +.dataviews-filters__container { + padding-top: 0; +} + .dataviews-filters__reset-button.dataviews-filters__reset-button[aria-disabled="true"] { &, &:hover { @@ -250,3 +254,29 @@ width: $icon-size; } } + +.dataviews-filters__container-visibility-toggle { + position: relative; + flex-shrink: 0; +} + +.dataviews-filters-toggle__count { + position: absolute; + top: 0; + right: 0; + transform: translate(50%, -50%); + background: var(--wp-admin-theme-color, #3858e9); + height: $grid-unit-20; + min-width: $grid-unit-20; + line-height: $grid-unit-20; + padding: 0 $grid-unit-05; + text-align: center; + border-radius: $grid-unit-10; + font-size: 11px; + outline: var(--wp-admin-border-width-focus) solid $white; + color: $white; +} + +.dataviews-search { + width: fit-content; +} diff --git a/packages/dataviews/src/components/dataviews-search/index.tsx b/packages/dataviews/src/components/dataviews-search/index.tsx index 5e5ce705e1119..1aef99872960b 100644 --- a/packages/dataviews/src/components/dataviews-search/index.tsx +++ b/packages/dataviews/src/components/dataviews-search/index.tsx @@ -30,15 +30,18 @@ const DataViewsSearch = memo( function Search( { label }: SearchProps ) { viewRef.current = view; }, [ onChangeView, view ] ); useEffect( () => { - onChangeViewRef.current( { - ...viewRef.current, - page: 1, - search: debouncedSearch, - } ); + if ( debouncedSearch !== viewRef.current?.search ) { + onChangeViewRef.current( { + ...viewRef.current, + page: 1, + search: debouncedSearch, + } ); + } }, [ debouncedSearch ] ); const searchLabel = label || __( 'Search' ); return ( ( { }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); const [ density, setDensity ] = useState< number >( 0 ); + const [ isShowingFilter, setIsShowingFilter ] = + useState< boolean >( false ); const isUncontrolled = selectionProperty === undefined || onChangeSelection === undefined; const selection = isUncontrolled ? selectionState : selectionProperty; @@ -90,6 +96,7 @@ export default function DataViews< Item >( { ); }, [ selection, data, getItemId ] ); + const filters = useFilters( _fields, view ); return ( ( { alignment="top" justify="start" className="dataviews__view-actions" + spacing={ 1 } > - + { search && } - + { view.type === LAYOUT_GRID && ( ( { { header } + { isShowingFilter && } diff --git a/packages/dataviews/src/components/dataviews/style.scss b/packages/dataviews/src/components/dataviews/style.scss index 6b8af6a90007d..742c8c42134df 100644 --- a/packages/dataviews/src/components/dataviews/style.scss +++ b/packages/dataviews/src/components/dataviews/style.scss @@ -9,7 +9,8 @@ flex-direction: column; } -.dataviews__view-actions { +.dataviews__view-actions, +.dataviews-filters__container { box-sizing: border-box; padding: $grid-unit-20 $grid-unit-60; flex-shrink: 0; @@ -78,7 +79,8 @@ /* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 430px) { - .dataviews__view-actions { + .dataviews__view-actions, + .dataviews-filters__container { padding: $grid-unit-15 $grid-unit-30; .components-search-control { @@ -95,3 +97,4 @@ padding-right: $grid-unit-30; } } + diff --git a/packages/dataviews/src/field-types/integer.tsx b/packages/dataviews/src/field-types/integer.tsx index 823983bc010bf..bf757776249b4 100644 --- a/packages/dataviews/src/field-types/integer.tsx +++ b/packages/dataviews/src/field-types/integer.tsx @@ -76,6 +76,8 @@ function Edit< Item >( { value={ value } options={ elements } onChange={ onChangeControl } + __next40pxDefaultSize + __nextHasNoMarginBottom /> ); } diff --git a/packages/dataviews/src/field-types/text.tsx b/packages/dataviews/src/field-types/text.tsx index 45d1b66bf71d9..227c70033cae0 100644 --- a/packages/dataviews/src/field-types/text.tsx +++ b/packages/dataviews/src/field-types/text.tsx @@ -1,8 +1,9 @@ /** * WordPress dependencies */ -import { TextControl } from '@wordpress/components'; +import { SelectControl, TextControl } from '@wordpress/components'; import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -47,6 +48,29 @@ function Edit< Item >( { [ id, onChange ] ); + if ( field.elements ) { + const elements = [ + /* + * Value can be undefined when: + * + * - the field is not required + * - in bulk editing + * + */ + { label: __( 'Select item' ), value: '' }, + ...field.elements, + ]; + + return ( + + ); + } + return ( ( { value={ value ?? '' } onChange={ onChangeControl } __next40pxDefaultSize + __nextHasNoMarginBottom /> ); } diff --git a/packages/dataviews/src/layouts/grid/index.tsx b/packages/dataviews/src/layouts/grid/index.tsx index cbfccf5521a81..c8cac31bf7db8 100644 --- a/packages/dataviews/src/layouts/grid/index.tsx +++ b/packages/dataviews/src/layouts/grid/index.tsx @@ -84,18 +84,18 @@ function GridItem< Item >( {
{ renderedMediaField }
+ - { renderedPrimaryField } diff --git a/packages/dataviews/src/layouts/grid/style.scss b/packages/dataviews/src/layouts/grid/style.scss index a707401972cda..ebe039ea7543b 100644 --- a/packages/dataviews/src/layouts/grid/style.scss +++ b/packages/dataviews/src/layouts/grid/style.scss @@ -9,6 +9,7 @@ .dataviews-view-grid__card { height: 100%; justify-content: flex-start; + position: relative; .dataviews-view-grid__title-actions { padding: $grid-unit-10 0 $grid-unit-05; @@ -22,6 +23,11 @@ .dataviews-view-grid__fields .dataviews-view-grid__field .dataviews-view-grid__field-value { color: $gray-900; } + + .dataviews-view-grid__media::after { + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); + box-shadow: inset 0 0 0 $border-width var(--wp-admin-theme-color); + } } } @@ -32,6 +38,7 @@ background-color: $gray-100; border-radius: $grid-unit-05; position: relative; + overflow: hidden; img { object-fit: cover; @@ -148,3 +155,16 @@ .dataviews-view-grid__field:empty { display: none; } + +.dataviews-view-grid__card .dataviews-selection-checkbox { + position: absolute; + top: -9999em; + left: $grid-unit-10; + z-index: z-index(".dataviews-view-grid__card .dataviews-selection-checkbox"); +} + +.dataviews-view-grid__card:hover .dataviews-selection-checkbox, +.dataviews-view-grid__card:focus-within .dataviews-selection-checkbox, +.dataviews-view-grid__card.is-selected .dataviews-selection-checkbox { + top: $grid-unit-10; +} diff --git a/packages/edit-site/src/components/add-new-post/index.js b/packages/edit-site/src/components/add-new-post/index.js index 7e75a47820fce..044e3c703b994 100644 --- a/packages/edit-site/src/components/add-new-post/index.js +++ b/packages/edit-site/src/components/add-new-post/index.js @@ -95,9 +95,10 @@ export default function AddNewPostModal( { postType, onSave, onClose } ) { size="small" >
- + } > + - - + + + + ); } diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index e63ed4ccebcf9..74bbee0524617 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -174,7 +174,12 @@ export default function PostList( { postType } ) { const [ view, setView ] = useView( postType ); const history = useHistory(); const location = useLocation(); - const { postId, quickEdit = false } = location.params; + const { + postId, + quickEdit = false, + isCustom, + activeView = 'all', + } = location.params; const [ selection, setSelection ] = useState( postId?.split( ',' ) ?? [] ); const onChangeSelection = useCallback( ( items ) => { @@ -334,6 +339,7 @@ export default function PostList( { postType } ) { } > { const { get: getPreference } = select( preferencesStore ); const { @@ -136,7 +135,7 @@ function Header( { ) } - diff --git a/packages/format-library/src/language/index.js b/packages/format-library/src/language/index.js index d37d8d6dbd0cd..6cfb8c4ad4492 100644 --- a/packages/format-library/src/language/index.js +++ b/packages/format-library/src/language/index.js @@ -13,6 +13,7 @@ import { Button, Popover, __experimentalHStack as HStack, + __experimentalVStack as VStack, } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { applyFormat, removeFormat, useAnchor } from '@wordpress/rich-text'; @@ -78,7 +79,9 @@ function InlineLanguageUI( { value, contentRef, onChange, onClose } ) { anchor={ popoverAnchor } onClose={ onClose } > -
{ event.preventDefault(); @@ -95,6 +98,8 @@ function InlineLanguageUI( { value, contentRef, onChange, onClose } ) { } } > setLang( val ) } @@ -103,6 +108,8 @@ function InlineLanguageUI( { value, contentRef, onChange, onClose } ) { ) } />