diff --git a/backport-changelog/7.1/11272.md b/backport-changelog/7.1/11272.md new file mode 100644 index 00000000000000..93a83d18e974b8 --- /dev/null +++ b/backport-changelog/7.1/11272.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/11272 + +* https://github.com/WordPress/gutenberg/pull/76573 diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php new file mode 100644 index 00000000000000..1fb8c82c63c550 --- /dev/null +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -0,0 +1,602 @@ +namespace = 'wp/v2'; + $this->rest_base = 'view-config'; + } + + /** + * Registers the routes for the controller. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'kind' => array( + 'description' => __( 'Entity kind.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ), + 'name' => array( + 'description' => __( 'Entity name.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks if a given request has access to read view config. + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( + // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $request + ) { + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to read view config.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Returns the default view configuration for the given entity type. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $kind = $request->get_param( 'kind' ); + $name = $request->get_param( 'name' ); + + // TODO: this data will come from a registry of view configs per entity. + $default_view = array( + 'type' => 'table', + 'filters' => array(), + 'perPage' => 20, + 'sort' => array( + 'field' => 'title', + 'direction' => 'asc', + ), + 'titleField' => 'title', + 'fields' => array( 'author', 'status' ), + ); + $default_layouts = array( + 'table' => array(), + 'grid' => array(), + 'list' => array(), + ); + $all_items_title = __( 'All items', 'gutenberg' ); + if ( 'postType' === $kind ) { + $post_type_object = get_post_type_object( $name ); + if ( $post_type_object && ! empty( $post_type_object->labels->all_items ) ) { + $all_items_title = $post_type_object->labels->all_items; + } + } + $view_list = array( + array( + 'title' => $all_items_title, + 'slug' => 'all', + ), + ); + if ( 'postType' === $kind && 'page' === $name ) { + $default_view = array( + 'type' => 'list', + 'filters' => array(), + 'perPage' => 20, + 'sort' => array( + 'field' => 'title', + 'direction' => 'asc', + ), + 'showLevels' => true, + 'titleField' => 'title', + 'mediaField' => 'featured_media', + 'fields' => array( 'author', 'status' ), + ); + $default_layouts = array( + 'table' => array( + 'layout' => array( + 'styles' => array( + 'author' => array( + 'align' => 'start', + ), + ), + ), + ), + 'grid' => array(), + 'list' => array(), + ); + $view_list = array( + array( + 'title' => $all_items_title, + 'slug' => 'all', + ), + array( + 'title' => __( 'Published', 'gutenberg' ), + 'slug' => 'published', + 'view' => array( + 'filters' => array( + array( + 'field' => 'status', + 'operator' => 'isAny', + 'value' => 'publish', + 'isLocked' => true, + ), + ), + ), + ), + array( + 'title' => __( 'Scheduled', 'gutenberg' ), + 'slug' => 'future', + 'view' => array( + 'filters' => array( + array( + 'field' => 'status', + 'operator' => 'isAny', + 'value' => 'future', + 'isLocked' => true, + ), + ), + ), + ), + array( + 'title' => __( 'Drafts', 'gutenberg' ), + 'slug' => 'drafts', + 'view' => array( + 'filters' => array( + array( + 'field' => 'status', + 'operator' => 'isAny', + 'value' => 'draft', + 'isLocked' => true, + ), + ), + ), + ), + array( + 'title' => __( 'Pending', 'gutenberg' ), + 'slug' => 'pending', + 'view' => array( + 'filters' => array( + array( + 'field' => 'status', + 'operator' => 'isAny', + 'value' => 'pending', + 'isLocked' => true, + ), + ), + ), + ), + array( + 'title' => __( 'Private', 'gutenberg' ), + 'slug' => 'private', + 'view' => array( + 'filters' => array( + array( + 'field' => 'status', + 'operator' => 'isAny', + 'value' => 'private', + 'isLocked' => true, + ), + ), + ), + ), + array( + 'title' => __( 'Trash', 'gutenberg' ), + 'slug' => 'trash', + 'view' => array( + 'type' => 'table', + 'layout' => isset( $default_layouts['table']['layout'] ) ? $default_layouts['table']['layout'] : array(), + 'filters' => array( + array( + 'field' => 'status', + 'operator' => 'isAny', + 'value' => 'trash', + 'isLocked' => true, + ), + ), + ), + ), + ); + } + + $response = array( + 'kind' => $kind, + 'name' => $name, + 'default_view' => $default_view, + 'default_layouts' => $default_layouts, + 'view_list' => $view_list, + ); + + return rest_ensure_response( $response ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $view_base_properties = $this->get_view_base_schema(); + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'view-config', + 'type' => 'object', + 'properties' => array( + 'kind' => array( + 'description' => __( 'Entity kind.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Entity name.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + ), + 'default_view' => array( + 'description' => __( 'Default view configuration.', 'gutenberg' ), + 'type' => 'object', + 'readonly' => true, + 'properties' => array_merge( + array( + 'type' => array( + 'type' => 'string', + ), + ), + $view_base_properties + ), + ), + 'default_layouts' => array( + 'description' => __( 'Default layout configurations.', 'gutenberg' ), + 'type' => 'object', + 'readonly' => true, + 'properties' => array( + 'table' => array( + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $this->get_table_layout_schema(), + ) + ), + ), + 'list' => array( + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $this->get_list_layout_schema(), + ) + ), + ), + 'grid' => array( + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $this->get_grid_layout_schema(), + ) + ), + ), + 'activity' => array( + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $this->get_list_layout_schema(), + ) + ), + ), + 'pickerGrid' => array( + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $this->get_grid_layout_schema(), + ) + ), + ), + 'pickerTable' => array( + 'type' => 'object', + 'properties' => array_merge( + $view_base_properties, + array( + 'layout' => $this->get_table_layout_schema(), + ) + ), + ), + ), + ), + 'view_list' => array( + 'description' => __( 'List of default views.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'title' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + 'view' => array( + 'type' => 'object', + 'properties' => array_merge( + array( + 'type' => array( + 'type' => 'string', + ), + 'layout' => $this->get_combined_layout_schema(), + ), + $view_base_properties + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Returns the schema properties shared by all view types (ViewBase), excluding 'type'. + * + * @return array Schema properties for the base view configuration. + */ + private function get_view_base_schema() { + return array( + 'search' => array( + 'type' => 'string', + ), + 'filters' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'type' => 'string', + ), + 'operator' => array( + 'type' => 'string', + 'enum' => array( + 'is', + 'isNot', + 'isAny', + 'isNone', + 'isAll', + 'isNotAll', + 'lessThan', + 'greaterThan', + 'lessThanOrEqual', + 'greaterThanOrEqual', + 'before', + 'after', + ), + ), + 'value' => array(), + 'isLocked' => array( + 'type' => 'boolean', + ), + ), + ), + ), + 'sort' => array( + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'type' => 'string', + ), + 'direction' => array( + 'type' => 'string', + 'enum' => array( 'asc', 'desc' ), + ), + ), + ), + 'page' => array( + 'type' => 'integer', + ), + 'perPage' => array( + 'type' => 'integer', + ), + 'fields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'titleField' => array( + 'type' => 'string', + ), + 'mediaField' => array( + 'type' => 'string', + ), + 'descriptionField' => array( + 'type' => 'string', + ), + 'showTitle' => array( + 'type' => 'boolean', + ), + 'showMedia' => array( + 'type' => 'boolean', + ), + 'showDescription' => array( + 'type' => 'boolean', + ), + 'showLevels' => array( + 'type' => 'boolean', + ), + 'groupBy' => array( + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'type' => 'string', + ), + 'direction' => array( + 'type' => 'string', + 'enum' => array( 'asc', 'desc' ), + ), + 'showLabel' => array( + 'type' => 'boolean', + 'default' => true, + ), + ), + ), + 'infiniteScrollEnabled' => array( + 'type' => 'boolean', + ), + ); + } + + /** + * Returns the schema for the ColumnStyle type. + * + * @return array Schema for a column style object. + */ + private function get_column_style_schema() { + return array( + 'type' => 'object', + 'properties' => array( + 'width' => array( + 'type' => array( 'string', 'number' ), + ), + 'maxWidth' => array( + 'type' => array( 'string', 'number' ), + ), + 'minWidth' => array( + 'type' => array( 'string', 'number' ), + ), + 'align' => array( + 'type' => 'string', + 'enum' => array( 'start', 'center', 'end' ), + ), + ), + ); + } + + /** + * Returns the layout schema for table-type views (ViewTable, ViewPickerTable). + * + * @return array Schema for a table layout object. + */ + private function get_table_layout_schema() { + return array( + 'type' => 'object', + 'properties' => array( + 'styles' => array( + 'type' => 'object', + 'additionalProperties' => $this->get_column_style_schema(), + ), + 'density' => array( + 'type' => 'string', + 'enum' => array( 'compact', 'balanced', 'comfortable' ), + ), + 'enableMoving' => array( + 'type' => 'boolean', + ), + ), + ); + } + + /** + * Returns the layout schema for list-type views (ViewList, ViewActivity). + * + * @return array Schema for a list layout object. + */ + private function get_list_layout_schema() { + return array( + 'type' => 'object', + 'properties' => array( + 'density' => array( + 'type' => 'string', + 'enum' => array( 'compact', 'balanced', 'comfortable' ), + ), + ), + ); + } + + /** + * Returns a combined layout schema that accepts properties from all view types. + * + * This is useful for contexts where the view type is not known ahead of time + * (e.g. the `view` override in a view list item), so all possible layout + * properties must be accepted. + * + * @return array Schema for a combined layout object. + */ + private function get_combined_layout_schema() { + return array( + 'type' => 'object', + 'properties' => array_merge( + $this->get_table_layout_schema()['properties'], + $this->get_grid_layout_schema()['properties'] + ), + ); + } + + /** + * Returns the layout schema for grid-type views (ViewGrid, ViewPickerGrid). + * + * @return array Schema for a grid layout object. + */ + private function get_grid_layout_schema() { + return array( + 'type' => 'object', + 'properties' => array( + 'badgeFields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'previewSize' => array( + 'type' => 'number', + ), + 'density' => array( + 'type' => 'string', + 'enum' => array( 'compact', 'balanced', 'comfortable' ), + ), + ), + ); + } +} diff --git a/lib/compat/wordpress-7.1/rest-api.php b/lib/compat/wordpress-7.1/rest-api.php index 4d7ceaba5cd85f..350ef1f08cdb1a 100644 --- a/lib/compat/wordpress-7.1/rest-api.php +++ b/lib/compat/wordpress-7.1/rest-api.php @@ -14,3 +14,12 @@ function gutenberg_register_icons_controller_endpoints() { $icons_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_icons_controller_endpoints', PHP_INT_MAX ); + +/** + * Registers the View Config REST API routes. + */ +function gutenberg_register_view_config_controller_endpoints() { + $view_config_controller = new Gutenberg_REST_View_Config_Controller_7_1(); + $view_config_controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_view_config_controller_endpoints', PHP_INT_MAX ); diff --git a/lib/load.php b/lib/load.php index e047840401ea68..88ef3408da2c90 100644 --- a/lib/load.php +++ b/lib/load.php @@ -82,6 +82,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 7.1 compat. require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php'; require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php'; + require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php'; require __DIR__ . '/compat/wordpress-7.1/rest-api.php'; // Plugin specific code. diff --git a/package-lock.json b/package-lock.json index f5922f480a3767..e5aff73105c212 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62771,10 +62771,12 @@ "version": "1.8.0", "license": "GPL-2.0-or-later", "dependencies": { + "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", "@wordpress/dataviews": "file:../dataviews", "@wordpress/element": "file:../element", "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", "dequal": "^2.0.3" }, "engines": { diff --git a/packages/core-data/src/private-actions.js b/packages/core-data/src/private-actions.js index 9da0a03c4d79ff..d2944fee3bd466 100644 --- a/packages/core-data/src/private-actions.js +++ b/packages/core-data/src/private-actions.js @@ -173,3 +173,21 @@ export const setCollaborationSupported = ( { dispatch } ) => { dispatch( { type: 'SET_COLLABORATION_SUPPORTED', supported } ); }; + +/** + * Returns an action object used to receive view config. + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {Object} config View config object. + * + * @return {Object} Action object. + */ +export function receiveViewConfig( kind, name, config ) { + return { + type: 'RECEIVE_VIEW_CONFIG', + kind, + name, + config, + }; +} diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 053f6fa5604874..76873c9a3ed1a5 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -314,3 +314,20 @@ export function getEditorAssets( state: State ): Record< string, any > | null { export function isCollaborationSupported( state: State ): boolean { return state.collaborationSupported; } + +/** + * Returns the view configuration for the given entity type. + * + * @param state Data state. + * @param kind Entity kind. + * @param name Entity name. + * + * @return The view configuration or undefined if not loaded. + */ +export function getViewConfig( + state: State, + kind: string, + name: string +): Record< string, any > | undefined { + return state.viewConfigs?.[ `${ kind }/${ name }` ]; +} diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 6ca294b08aaf69..3f7c80894b1a56 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -721,6 +721,25 @@ export function collaborationSupported( state = true, action ) { return state; } +/** + * Reducer managing view configs, keyed by `kind/name`. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function viewConfigs( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_VIEW_CONFIG': + return { + ...state, + [ `${ action.kind }/${ action.name }` ]: action.config, + }; + } + return state; +} + export default combineReducers( { users, currentTheme, @@ -745,4 +764,5 @@ export default combineReducers( { editorAssets, syncConnectionStatuses, collaborationSupported, + viewConfigs, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 42b16a60422ec4..a08e25cb18b4e5 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -1320,3 +1320,18 @@ export const getEditorAssets = } ); dispatch.receiveEditorAssets( assets ); }; + +/** + * Requests view config for a given entity type from the REST API. + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + */ +export const getViewConfig = + ( kind, name ) => + async ( { dispatch } ) => { + const config = await apiFetch( { + path: addQueryArgs( '/wp/v2/view-config', { kind, name } ), + } ); + dispatch.receiveViewConfig( kind, name, config ); + }; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index f5eee8f1214e63..195d2e74acb4ca 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -55,6 +55,7 @@ export interface State { editorAssets: Record< string, any > | null; syncConnectionStatuses?: Record< string, ConnectionStatus >; collaborationSupported: boolean; + viewConfigs: Record< string, Record< string, any > >; } type EntityRecordKey = string | number; diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index d6a56a1676fba0..dd9e0f2e0ae871 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -14,7 +14,7 @@ import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useEvent, usePrevious } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; -import { useView } from '@wordpress/views'; +import { useView, useViewConfig } from '@wordpress/views'; /** * Internal dependencies @@ -33,11 +33,7 @@ import { useEditPostAction, useQuickEditPostAction, } from '../dataviews-actions'; -import { - defaultLayouts, - DEFAULT_VIEW, - getActiveViewOverridesForTab, -} from './view-utils'; + import useNotesCount from './use-notes-count'; import { QuickEditModal } from './quick-edit-modal'; @@ -60,16 +56,24 @@ export default function PostList( { postType } ) { const { path, query } = useLocation(); const { activeView = 'all', postId, quickEdit = false } = query; const history = useHistory(); - const defaultView = DEFAULT_VIEW; + const { + default_view: defaultView, + default_layouts: defaultLayouts, + view_list: viewList, + } = useViewConfig( { + kind: 'postType', + name: postType, + } ); const activeViewOverrides = useMemo( - () => getActiveViewOverridesForTab( activeView ), - [ activeView ] + () => viewList?.find( ( v ) => v.slug === activeView )?.view ?? {}, + [ viewList, activeView ] ); const { view, updateView, isModified, resetToDefault } = useView( { kind: 'postType', name: postType, slug: 'default', defaultView, + defaultLayouts, activeViewOverrides, queryParams: { page: query.pageNumber, @@ -317,7 +321,7 @@ export default function PostList( { postType } ) { } } getItemId={ getItemId } getItemLevel={ getItemLevel } - defaultLayouts={ defaultLayouts } + defaultLayouts={ defaultLayouts ?? {} } onReset={ isModified ? () => { diff --git a/packages/edit-site/src/components/post-list/view-utils.js b/packages/edit-site/src/components/post-list/view-utils.js deleted file mode 100644 index 2f2ceb0c7673b0..00000000000000 --- a/packages/edit-site/src/components/post-list/view-utils.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -import { - trash, - pages, - drafts, - published, - scheduled, - pending, - notAllowed, -} from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { OPERATOR_IS_ANY } from '../../utils/constants'; - -export const defaultLayouts = { - table: { - layout: { - styles: { - author: { - align: 'start', - }, - }, - }, - }, - grid: {}, - list: {}, -}; - -export const DEFAULT_VIEW = { - type: 'list', - filters: [], - perPage: 20, - sort: { - field: 'title', - direction: 'asc', - }, - showLevels: true, - titleField: 'title', - mediaField: 'featured_media', - fields: [ 'author', 'status' ], - ...defaultLayouts.list, -}; - -export function getDefaultViews( postType ) { - return [ - { - title: postType?.labels?.all_items || __( 'All items' ), - slug: 'all', - icon: pages, - view: DEFAULT_VIEW, - }, - { - title: __( 'Published' ), - slug: 'published', - icon: published, - view: { - ...DEFAULT_VIEW, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'publish', - isLocked: true, - }, - ], - }, - }, - { - title: __( 'Scheduled' ), - slug: 'future', - icon: scheduled, - view: { - ...DEFAULT_VIEW, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'future', - isLocked: true, - }, - ], - }, - }, - { - title: __( 'Drafts' ), - slug: 'drafts', - icon: drafts, - view: { - ...DEFAULT_VIEW, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'draft', - isLocked: true, - }, - ], - }, - }, - { - title: __( 'Pending' ), - slug: 'pending', - icon: pending, - view: { - ...DEFAULT_VIEW, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'pending', - isLocked: true, - }, - ], - }, - }, - { - title: __( 'Private' ), - slug: 'private', - icon: notAllowed, - view: { - ...DEFAULT_VIEW, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'private', - isLocked: true, - }, - ], - }, - }, - { - title: __( 'Trash' ), - slug: 'trash', - icon: trash, - view: { - ...DEFAULT_VIEW, - type: 'table', - layout: defaultLayouts.table.layout, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'trash', - isLocked: true, - }, - ], - }, - }, - ]; -} - -const SLUG_TO_STATUS = { - published: 'publish', - future: 'future', - drafts: 'draft', - pending: 'pending', - private: 'private', - trash: 'trash', -}; - -export function getActiveViewOverridesForTab( activeView ) { - const base = { - ...defaultLayouts.table, - }; - const status = SLUG_TO_STATUS[ activeView ]; - if ( ! status ) { - return base; - } - return { - ...base, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: status, - isLocked: true, - }, - ], - }; -} diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js index 084cace7acb835..2996d9d59e09be 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -3,34 +3,43 @@ */ import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { useMemo } from '@wordpress/element'; +import { + trash, + pages, + drafts, + published, + scheduled, + pending, + notAllowed, +} from '@wordpress/icons'; +import { useViewConfig } from '@wordpress/views'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; import DataViewItem from './dataview-item'; -import { getDefaultViews } from '../post-list/view-utils'; const { useLocation } = unlock( routerPrivateApis ); +const SLUG_TO_ICON = { + all: pages, + published, + future: scheduled, + drafts, + pending, + private: notAllowed, + trash, +}; + export default function DataViewsSidebarContent( { postType } ) { const { query: { activeView = 'all' }, } = useLocation(); - const postTypeObject = useSelect( - ( select ) => { - const { getPostType } = select( coreStore ); - return getPostType( postType ); - }, - [ postType ] - ); - const defaultViews = useMemo( - () => getDefaultViews( postTypeObject ), - [ postTypeObject ] - ); + const { default_view: defaultView, view_list: viewList } = useViewConfig( { + kind: 'postType', + name: postType, + } ); if ( ! postType ) { return null; } @@ -38,15 +47,15 @@ export default function DataViewsSidebarContent( { postType } ) { return ( <> - { defaultViews.map( ( dataview ) => { + { viewList?.map( ( view ) => { return ( ); } ) } diff --git a/packages/edit-site/src/components/site-editor-routes/pages.js b/packages/edit-site/src/components/site-editor-routes/pages.js index ca14fe6781a28d..4c93b2b049da16 100644 --- a/packages/edit-site/src/components/site-editor-routes/pages.js +++ b/packages/edit-site/src/components/site-editor-routes/pages.js @@ -3,6 +3,8 @@ */ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { __ } from '@wordpress/i18n'; +import { resolveSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; import { loadView } from '@wordpress/views'; /** @@ -14,21 +16,25 @@ import SidebarNavigationScreenUnsupported from '../sidebar-navigation-screen-uns import DataViewsSidebarContent from '../sidebar-dataviews'; import PostList from '../post-list'; import { unlock } from '../../lock-unlock'; -import { - DEFAULT_VIEW, - getActiveViewOverridesForTab, -} from '../post-list/view-utils'; const { useLocation } = unlock( routerPrivateApis ); async function isListView( query ) { const { activeView = 'all' } = query; + const config = await unlock( resolveSelect( coreStore ) ).getViewConfig( + 'postType', + 'page' + ); + const defaultView = config?.default_view; + const defaultLayouts = config?.default_layouts; + const viewEntry = config?.view_list?.find( ( v ) => v.slug === activeView ); const view = await loadView( { kind: 'postType', name: 'page', slug: 'default', - defaultView: DEFAULT_VIEW, - activeViewOverrides: getActiveViewOverridesForTab( activeView ), + defaultView, + defaultLayouts, + activeViewOverrides: viewEntry?.view ?? {}, } ); return view.type === 'list'; } diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts index 21c9e4d1c034b9..5fda5338eb0086 100644 --- a/packages/private-apis/src/implementation.ts +++ b/packages/private-apis/src/implementation.ts @@ -46,6 +46,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/upload-media', '@wordpress/global-styles-ui', '@wordpress/ui', + '@wordpress/views', ]; /* diff --git a/packages/views/README.md b/packages/views/README.md index c46ef53743e4ba..9b42e513d18a4f 100644 --- a/packages/views/README.md +++ b/packages/views/README.md @@ -51,4 +51,18 @@ _Returns_ - `UseViewReturn`: Object with current view, modification state, and update functions. +### useViewConfig + +A hook that retrieves the view configuration for a given entity from the core data store. + +_Parameters_ + +- _params_ `Object`: +- _params.kind_ `string`: The kind of the entity. +- _params.name_ `string`: The name of the entity. + +_Returns_ + +- `Object`: An object containing the `default_view`, `default_layouts`, and `view_list` configuration for the entity. + diff --git a/packages/views/package.json b/packages/views/package.json index 4b1214f379df1f..267d63932a2037 100644 --- a/packages/views/package.json +++ b/packages/views/package.json @@ -41,10 +41,12 @@ }, "types": "build-types", "dependencies": { + "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", "@wordpress/dataviews": "file:../dataviews", "@wordpress/element": "file:../element", "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", "dequal": "^2.0.3" }, "publishConfig": { diff --git a/packages/views/src/index.ts b/packages/views/src/index.ts index bba6bbbec838ef..f988b13a583be3 100644 --- a/packages/views/src/index.ts +++ b/packages/views/src/index.ts @@ -1,2 +1,3 @@ export { useView } from './use-view'; export { loadView } from './load-view'; +export { useViewConfig } from './use-view-config'; diff --git a/packages/views/src/load-view.ts b/packages/views/src/load-view.ts index 0084f87672c3d0..bf7f8cff28a77a 100644 --- a/packages/views/src/load-view.ts +++ b/packages/views/src/load-view.ts @@ -38,13 +38,19 @@ export async function loadView( config: ViewConfig ) { const page = queryParams?.page ?? 1; const search = queryParams?.search ?? ''; + const layoutTypeDefaults = + config.defaultLayouts?.[ + baseView?.type as keyof typeof config.defaultLayouts + ] ?? {}; + const combinedOverrides = { ...layoutTypeDefaults, ...activeViewOverrides }; + return mergeActiveViewOverrides( { ...baseView, page, search, }, - activeViewOverrides, + combinedOverrides, defaultView ); } diff --git a/packages/views/src/lock-unlock.ts b/packages/views/src/lock-unlock.ts new file mode 100644 index 00000000000000..4eda1b2ae85eb3 --- /dev/null +++ b/packages/views/src/lock-unlock.ts @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/views' + ); diff --git a/packages/views/src/types.ts b/packages/views/src/types.ts index b1bb10ecbb07a5..7a5260d585e976 100644 --- a/packages/views/src/types.ts +++ b/packages/views/src/types.ts @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import type { View } from '@wordpress/dataviews'; +import type { View, SupportedLayouts } from '@wordpress/dataviews'; export type ActiveViewOverrides = { // scalar values @@ -51,6 +51,13 @@ export interface ViewConfig { */ activeViewOverrides?: ActiveViewOverrides; + /** + * Default layout configurations keyed by layout type. + * Used to apply layout-type-specific defaults (e.g., table column styles) + * that are merged as overrides based on the resolved view type. + */ + defaultLayouts?: SupportedLayouts; + /** * Optional query parameters from URL (page, search) */ diff --git a/packages/views/src/use-view-config.ts b/packages/views/src/use-view-config.ts new file mode 100644 index 00000000000000..ca1445921c49d7 --- /dev/null +++ b/packages/views/src/use-view-config.ts @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import type { View, SupportedLayouts } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { unlock } from './lock-unlock'; + +/** + * A hook that retrieves the view configuration for a given entity + * from the core data store. + * + * @param {Object} params + * @param {string} params.kind The kind of the entity. + * @param {string} params.name The name of the entity. + * @return {Object} An object containing the `default_view`, `default_layouts`, and `view_list` configuration for the entity. + */ +export function useViewConfig( { + kind, + name, +}: { + kind: string; + name: string; +} ): { + default_view: View; + default_layouts: SupportedLayouts; + view_list: Array< any >; +} { + return useSelect( + ( select ) => { + return unlock( select( coreStore ) ).getViewConfig( kind, name ); + }, + [ kind, name ] + ); +} diff --git a/packages/views/src/use-view.ts b/packages/views/src/use-view.ts index 0306f8e399c839..548e932439e856 100644 --- a/packages/views/src/use-view.ts +++ b/packages/views/src/use-view.ts @@ -70,10 +70,18 @@ export function useView( config: ViewConfig ): UseViewReturn { ); const { set } = useDispatch( preferencesStore ); - const baseView: View = persistedView ?? defaultView; + const baseView: View = persistedView ?? defaultView ?? {}; const page = Number( queryParams?.page ?? baseView.page ?? 1 ); const search = queryParams?.search ?? baseView.search ?? ''; + const combinedOverrides = useMemo( () => { + const layoutTypeDefaults = + config.defaultLayouts?.[ + baseView.type as keyof typeof config.defaultLayouts + ] ?? {}; + return { ...layoutTypeDefaults, ...activeViewOverrides }; + }, [ config.defaultLayouts, baseView.type, activeViewOverrides ] ); + // Merge URL query parameters (page, search) and activeViewOverrides into the view const view: View = useMemo( () => { return mergeActiveViewOverrides( @@ -82,10 +90,10 @@ export function useView( config: ViewConfig ): UseViewReturn { page, search, }, - activeViewOverrides, + combinedOverrides, defaultView ); - }, [ baseView, page, search, activeViewOverrides, defaultView ] ); + }, [ baseView, page, search, combinedOverrides, defaultView ] ); const isModified = !! persistedView; @@ -100,7 +108,7 @@ export function useView( config: ViewConfig ): UseViewReturn { // Cast is safe: omitting page/search doesn't change the discriminant (type field) const preferenceView = stripActiveViewOverrides( omit( newView, [ 'page', 'search' ] ) as View, - activeViewOverrides, + combinedOverrides, defaultView ); @@ -115,12 +123,12 @@ export function useView( config: ViewConfig ): UseViewReturn { // Compare with baseView and defaultView after stripping activeViewOverrides const comparableBaseView = stripActiveViewOverrides( baseView, - activeViewOverrides, + combinedOverrides, defaultView ); const comparableDefaultView = stripActiveViewOverrides( defaultView, - activeViewOverrides, + combinedOverrides, defaultView ); @@ -139,7 +147,7 @@ export function useView( config: ViewConfig ): UseViewReturn { search, baseView, defaultView, - activeViewOverrides, + combinedOverrides, set, preferenceKey, ] diff --git a/packages/views/tsconfig.json b/packages/views/tsconfig.json index 5c77d8fc731bf8..ed8b1f176bc2ad 100644 --- a/packages/views/tsconfig.json +++ b/packages/views/tsconfig.json @@ -2,9 +2,11 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "references": [ + { "path": "../core-data" }, { "path": "../data" }, { "path": "../dataviews" }, { "path": "../element" }, - { "path": "../preferences" } + { "path": "../preferences" }, + { "path": "../private-apis" } ] }