From 57c5f849a5fc6ef6b3ba3f90e52c66fbe4ca8c5e Mon Sep 17 00:00:00 2001 From: Dan Knauss <273554+dknauss@users.noreply.github.com> Date: Mon, 9 Dec 2024 01:46:06 -0700 Subject: [PATCH 01/50] Improve DataViews README (#67711) Co-authored-by: dknauss Co-authored-by: oandregal --- packages/dataviews/README.md | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 651d87268c28e1..6f74a13d8f197a 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1,6 +1,6 @@ # The `@wordpress/dataviews` package -The DataViews package offers two React components and a few utilites to work with a list of data: +The DataViews package offers two React components and a few utilities to work with a list of data: - `DataViews`: to render the dataset using different types of layouts (table, grid, list) and interaction capabilities (search, filters, sorting, etc.). - `DataForm`: to edit the items of the dataset. @@ -15,7 +15,7 @@ npm install @wordpress/dataviews --save ## `DataViews` - + **Important note** If you're trying to use the `DataViews` component in a WordPress plugin or theme and you're building your scripts using the `@wordpress/scripts` package, you need to import the components from `@wordpress/dataviews/wp` instead of `@wordpress/dataviews`. @@ -68,13 +68,13 @@ const data = [ ]; ``` -The data can come from anywhere, from a static JSON file to a dynamic source like a HTTP Request. It's the consumer's responsiblity to query the data source appropiately and update the dataset based on the user's choices for sorting, filtering, etc. +The data can come from anywhere, from a static JSON file to a dynamic source like an HTTP Request. It's the consumer's responsibility to query the data source appropriately and update the dataset based on the user's choices for sorting, filtering, etc. Each record should have an `id` that identifies them uniquely. If they don't, the consumer should provide the `getItemId` property to `DataViews`: a function that returns an unique identifier for the record. #### `getItemId`: `function` -Function that receives an item and returns an unique identifier for it. +A function that receives an item and returns a unique identifier for it. It's optional. The field will get a default implementation by `DataViews` that returns the value of the `item[ id ]`. @@ -185,9 +185,9 @@ Properties: - `field`: the field used for sorting the dataset. - `direction`: the direction to use for sorting, one of `asc` or `desc`. -- `titleField`: The id of the field reprensenting the title of the record. -- `mediaField`: The id of the field reprensenting the media of the record. -- `descriptionField`: The id of field the reprensenting the description of the record. +- `titleField`: The id of the field representing the title of the record. +- `mediaField`: The id of the field representing the media of the record. +- `descriptionField`: The id of the field representing the description of the record. - `showTitle`: Whether the title should be shown in the UI. `true` by default. - `showMedia`: Whether the media should be shown in the UI. `true` by default. - `showDescription`: Whether the description should be shown in the UI. `true` by default. @@ -205,7 +205,7 @@ Properties: Callback executed when the view has changed. It receives the new view object as a parameter. -The view is a representation of the visible state of the dataset: what type of layout is used to display it (table, grid, etc.), how the dataset is filtered, how it is sorted or paginated. It's the consumer's responsibility to use the view config to query the data provider and make sure the user decisions (sort, pagination, filters, etc.) are respected. +The view is a representation of the visible state of the dataset: what type of layout is used to display it (table, grid, etc.), how the dataset is filtered, and how it is sorted or paginated. The consumer is responsible for using the view config to query the data provider and ensure the user decisions (sort, pagination, filters, etc.) are respected. The following example shows how a view object is used to query the WordPress REST API via the entities abstraction. The same can be done with any other data provider. @@ -337,7 +337,7 @@ Whether the data is loading. `false` by default. #### `defaultLayouts`: `Record< string, view >` -This property provides layout information about the view types that are active. If empty, enables all layout types (see "Layout Types") with empty layout data. +This property provides layout information about active view types. If empty, this enables all layout types (see "Layout Types") with empty layout data. For example, this is how you'd enable only the table view type: @@ -358,17 +358,17 @@ The `defaultLayouts` property should be an object that includes properties named The list of selected items' ids. -If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves as a controlled component, otherwise, it behaves like an uncontrolled component. +If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component. #### `onChangeSelection`: `function` -Callback that signals the user selected one of more items. It receives the list of selected items' ids as a parameter. +Callback that signals the user selected one of more items. It receives the list of selected items' IDs as a parameter. -If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves as a controlled component, otherwise, it behaves like an uncontrolled component. +If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component. ### `isItemClickable`: `function` -A function that determines if a media field or a primary field are clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. +A function that determines if a media field or a primary field is clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. ### `onClickItem`: `function` @@ -405,7 +405,7 @@ const Example = () => { A single item to be edited. -It can be think of as a single record coming from the `data` property of `DataViews` — though it doesn't need to be. It can be totally separated or a mix of records if your app supports bulk editing. +It can be thought of as a single record coming from the `data` property of `DataViews` — though it doesn't need to be. It can be totally separated or a mix of records if your app supports bulk editing. #### `fields`: `Object[]` @@ -500,7 +500,7 @@ Returns an object containing: ### `isItemValid` -Utility to determine whether or not the given item's value is valid according to the current fields and form config. +Utility is used to determine whether or not the given item's value is valid according to the current fields and form configuration. Parameters: @@ -687,7 +687,7 @@ Example: Field type. One of `text`, `integer`, `datetime`. -If a field declares a `type`, it gets default implementations for the `sort`, `isValid`, and `Edit` functions. They will overriden if the field provides its own. +If a field declares a `type`, it gets default implementations for the `sort`, `isValid`, and `Edit` functions if no other values are specified. - Type: `string`. - Optional. @@ -732,7 +732,7 @@ Example: ### `getValue` -React component that returns the value of a field. This value is used in sorting the fields, or when filtering. +React component that returns the value of a field. This value is used to sort or filter the fields. - Type: React component. - Optional. @@ -977,13 +977,13 @@ Example: ### `elements` -List of valid values for a field. If provided, it creates a DataViews' filter for the field. DataForm's edit control will use these values as well (see `Edit` field property). +List of valid values for a field. If provided, it creates a DataViews' filter for the field. DataForm's edit control will also use these values. (See `Edit` field property.) - Type: `array` of objects. - Optional. - Each object can have the following properties: - - `value`: required, the value to match against the field's value. - - `label`: required, the name to display to users. + - `value`: the value to match against the field's value. (Required) + - `label`: the name to display to users. (Required) - `description`: optional, a longer description of the item. Example: @@ -1006,7 +1006,7 @@ Configuration of the filters. - Type: `object`. - Optional. - Properties: - - `operators`: the list of operators supported by the field. See "operators" below. By default, a filter will support the `isAny` and `isNone` multi-selection operators. + - `operators`: the list of operators supported by the field. See "operators" below. A filter will support the `isAny` and `isNone` multi-selection operators by default. - `isPrimary`: boolean, optional. Indicates if the filter is primary. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. Operators: @@ -1020,7 +1020,7 @@ Operators: | `isAll` | Multiple items | `AND`. The item's field has all of the values in the list. | Category is all: Book, Review, Science Fiction | | `isNotAll` | Multiple items | `NOT AND`. The item's field doesn't have all of the values in the list. | Category is not all: Book, Review, Science Fiction | -`is` and `isNot` are single-selection operators, while `isAny`, `isNone`, `isAll`, and `isNotALl` are multi-selection. By default, a filter with no operators declared will support the `isAny` and `isNone` multi-selection operators. A filter cannot mix single-selection & multi-selection operators; if a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded and the filter won't allow selecting more than one item. +`is` and `isNot` are single-selection operators, while `isAny`, `isNone`, `isAll`, and `isNotALl` are multi-selection. A filter with no operators declared will support the `isAny` and `isNone` multi-selection operators by default. A filter cannot mix single-selection & multi-selection operators; if a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded, and the filter won't allow selecting more than one item. Example: From 9e0397303f23d9540e2d6aae2a7aeb30268bd3ce Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 9 Dec 2024 20:45:04 +1100 Subject: [PATCH 02/50] Data views: expand config drop down on mobile (#67715) In this view, ensure that the content is horizontally scrollable and the width takes up the screen.Co-authored-by: ramonjd Co-authored-by: ntsekouras Co-authored-by: megane9988 --- .../src/components/dataviews-view-config/index.tsx | 12 ++++++++++-- .../src/components/dataviews-view-config/style.scss | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 28e48525ffa73b..3146064a41922b 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -53,7 +53,11 @@ interface ViewTypeMenuProps { defaultLayouts?: SupportedLayouts; } -const DATAVIEWS_CONFIG_POPOVER_PROPS = { placement: 'bottom-end', offset: 9 }; +const DATAVIEWS_CONFIG_POPOVER_PROPS = { + className: 'dataviews-config__popover', + placement: 'bottom-end', + offset: 9, +}; function ViewTypeMenu( { defaultLayouts = { list: {}, grid: {}, table: {} }, @@ -619,6 +623,7 @@ function DataviewsViewConfigDropdown() { ); return ( ( - + diff --git a/packages/dataviews/src/components/dataviews-view-config/style.scss b/packages/dataviews/src/components/dataviews-view-config/style.scss index 8427796b6e15ed..0fd97b916b4aa8 100644 --- a/packages/dataviews/src/components/dataviews-view-config/style.scss +++ b/packages/dataviews/src/components/dataviews-view-config/style.scss @@ -6,6 +6,14 @@ line-height: $default-line-height; } +.dataviews-config__popover.is-expanded .dataviews-config__popover-content-wrapper { + overflow-y: scroll; + height: 100%; + .dataviews-view-config { + width: auto; + } +} + .dataviews-view-config__sort-direction .components-toggle-group-control-option-base { text-transform: uppercase; } From ce01840f5d937ce13f780a19bfb7deba9bfa9ea6 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 9 Dec 2024 14:34:28 +0400 Subject: [PATCH 03/50] Data: Include more details when shallow equality fails in 'useSelect' (#67713) Co-authored-by: Mamaduka Co-authored-by: jsnajdr Co-authored-by: tyxla --- .../data/src/components/use-select/index.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 15a3c88d2d5543..6931743151773c 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -19,6 +19,25 @@ import useAsyncMode from '../async-mode-provider/use-async-mode'; const renderQueue = createQueue(); +function warnOnUnstableReference( a, b ) { + if ( ! a || ! b ) { + return; + } + + const keys = + typeof a === 'object' && typeof b === 'object' + ? Object.keys( a ).filter( ( k ) => a[ k ] !== b[ k ] ) + : []; + + // eslint-disable-next-line no-console + console.warn( + 'The `useSelect` hook returns different values when called with the same state and parameters.\n' + + 'This can lead to unnecessary re-renders and performance issues if not fixed.\n\n' + + 'Non-equal value keys: %s\n\n', + keys.join( ', ' ) + ); +} + /** * @typedef {import('../../types').StoreDescriptor} StoreDescriptor * @template {import('../../types').AnyConfig} C @@ -159,10 +178,7 @@ function Store( registry, suspense ) { if ( ! didWarnUnstableReference ) { const secondMapResult = mapSelect( select, registry ); if ( ! isShallowEqual( mapResult, secondMapResult ) ) { - // eslint-disable-next-line no-console - console.warn( - `The 'useSelect' hook returns different values when called with the same state and parameters. This can lead to unnecessary rerenders.` - ); + warnOnUnstableReference( mapResult, secondMapResult ); didWarnUnstableReference = true; } } From 0d757db8ccb47988d9d3655c2b3ca86c1a6072dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=A4gy?= Date: Mon, 9 Dec 2024 11:45:07 +0100 Subject: [PATCH 04/50] Feature: add ability to show drop cap setting in paragraph block by default via defaultControls config (#45994) Co-authored-by: fabiankaegy Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: aaronrobertshaw Co-authored-by: jasmussen Co-authored-by: carolinan Co-authored-by: jeffpaul Co-authored-by: jordesign --- packages/block-library/src/paragraph/edit.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index d32b4e8d5eca02..02ca1feceae555 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -21,6 +21,7 @@ import { useSettings, useBlockEditingMode, } from '@wordpress/block-editor'; +import { getBlockSupport } from '@wordpress/blocks'; import { formatLtr } from '@wordpress/icons'; /** @@ -47,7 +48,7 @@ function hasDropCapDisabled( align ) { return align === ( isRTL() ? 'left' : 'right' ) || align === 'center'; } -function DropCapControl( { clientId, attributes, setAttributes } ) { +function DropCapControl( { clientId, attributes, setAttributes, name } ) { // Please do not add a useSelect call to the paragraph block unconditionally. // Every useSelect added to a (frequently used) block will degrade load // and type performance. By moving it within InspectorControls, the subscription is @@ -69,11 +70,18 @@ function DropCapControl( { clientId, attributes, setAttributes } ) { helpText = __( 'Show a large initial letter.' ); } + const isDropCapControlEnabledByDefault = getBlockSupport( + name, + 'typography.defaultControls.dropCap', + false + ); + return ( !! dropCap } label={ __( 'Drop cap' ) } + isShownByDefault={ isDropCapControlEnabledByDefault } onDeselect={ () => setAttributes( { dropCap: undefined } ) } resetAllFilter={ () => ( { dropCap: undefined } ) } panelId={ clientId } @@ -99,6 +107,7 @@ function ParagraphBlock( { setAttributes, clientId, isSelected: isSingleSelected, + name, } ) { const { align, content, direction, dropCap, placeholder } = attributes; const blockProps = useBlockProps( { @@ -136,6 +145,7 @@ function ParagraphBlock( { ) } { isSingleSelected && ( Date: Mon, 9 Dec 2024 11:46:54 +0100 Subject: [PATCH 05/50] Feature: Allow Post Template block to get deeply nested within Query Block (#67657) Co-authored-by: fabiankaegy Co-authored-by: carolinan Co-authored-by: youknowriad Co-authored-by: Mamaduka Co-authored-by: ntsekouras --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/post-template/block.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 29033e278c3488..71ad6359adca0a 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -660,7 +660,7 @@ Contains the block elements used to render a post, like the title, date, feature - **Name:** core/post-template - **Category:** theme -- **Parent:** core/query +- **Ancestor:** core/query - **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ ## Post Terms diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index da576a83312a45..6e1f58155590f3 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -4,7 +4,7 @@ "name": "core/post-template", "title": "Post Template", "category": "theme", - "parent": [ "core/query" ], + "ancestor": [ "core/query" ], "description": "Contains the block elements used to render a post, like the title, date, featured image, content or excerpt, and more.", "textdomain": "default", "usesContext": [ From 5e3d5752cca4e9005f09b1c2a8ec1a1ff89275bc Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 9 Dec 2024 13:38:06 +0200 Subject: [PATCH 06/50] DataViews: Refactor actions to render modal outside of the menu (#67664) Co-authored-by: ntsekouras Co-authored-by: oandregal Co-authored-by: ciampo Co-authored-by: doekenorg --- .../dataviews-bulk-actions/index.tsx | 46 ++++- .../dataviews-item-actions/index.tsx | 186 ++++++++---------- .../src/dataviews-layouts/list/index.tsx | 14 ++ .../src/components/post-actions/index.js | 133 +++++-------- 4 files changed, 194 insertions(+), 185 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx b/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx index 92a3fe85f67e74..86f0bb6db0ba84 100644 --- a/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-bulk-actions/index.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import type { ReactElement } from 'react'; + /** * WordPress dependencies */ @@ -15,11 +20,46 @@ import { closeSmall } from '@wordpress/icons'; * Internal dependencies */ import DataViewsContext from '../dataviews-context'; -import { ActionWithModal } from '../dataviews-item-actions'; -import type { Action } from '../../types'; +import { ActionModal } from '../dataviews-item-actions'; +import type { Action, ActionModal as ActionModalType } from '../../types'; import type { SetSelection } from '../../private-types'; import type { ActionTriggerProps } from '../dataviews-item-actions'; +interface ActionWithModalProps< Item > { + action: ActionModalType< Item >; + items: Item[]; + ActionTriggerComponent: ( + props: ActionTriggerProps< Item > + ) => ReactElement; +} + +function ActionWithModal< Item >( { + action, + items, + ActionTriggerComponent, +}: ActionWithModalProps< Item > ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const actionTriggerProps = { + action, + onClick: () => { + setIsModalOpen( true ); + }, + items, + }; + return ( + <> + + { isModalOpen && ( + setIsModalOpen( false ) } + /> + ) } + + ); +} + export function useHasAPossibleBulkAction< Item >( actions: Action< Item >[], item: Item @@ -160,7 +200,7 @@ function ActionButton< Item >( { key={ action.id } action={ action } items={ selectedEligibleItems } - ActionTrigger={ ActionTrigger } + ActionTriggerComponent={ ActionTrigger } /> ); } diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index c5e1cb09adf15f..abe63e27a15b3b 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { MouseEventHandler, ReactElement } from 'react'; +import type { MouseEventHandler } from 'react'; /** * WordPress dependencies @@ -32,20 +32,17 @@ export interface ActionTriggerProps< Item > { items: Item[]; } -interface ActionModalProps< Item > { +export interface ActionModalProps< Item > { action: ActionModalType< Item >; items: Item[]; - closeModal?: () => void; -} - -interface ActionWithModalProps< Item > extends ActionModalProps< Item > { - ActionTrigger: ( props: ActionTriggerProps< Item > ) => ReactElement; - isBusy?: boolean; + closeModal: () => void; } interface ActionsMenuGroupProps< Item > { actions: Action< Item >[]; item: Item; + registry: ReturnType< typeof useRegistry >; + setActiveModalAction: ( action: ActionModalType< Item > | null ) => void; } interface ItemActionsProps< Item > { @@ -58,6 +55,7 @@ interface CompactItemActionsProps< Item > { item: Item; actions: Action< Item >[]; isSmall?: boolean; + registry: ReturnType< typeof useRegistry >; } interface PrimaryActionsProps< Item > { @@ -65,12 +63,6 @@ interface PrimaryActionsProps< Item > { actions: Action< Item >[]; registry: ReturnType< typeof useRegistry >; } -interface ActionsListProps< Item > { - item: Item; - actions: Action< Item >[]; - registry: ReturnType< typeof useRegistry >; - ActionTrigger: ( props: ActionTriggerProps< Item > ) => ReactElement; -} function ButtonTrigger< Item >( { action, @@ -98,10 +90,7 @@ function MenuItemTrigger< Item >( { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - + { label } ); @@ -118,7 +107,7 @@ export function ActionModal< Item >( { {} ) } + onRequestClose={ closeModal } focusOnMount="firstContentElement" size="medium" overlayClassName={ `dataviews-action-modal dataviews-action-modal__${ kebabCase( @@ -130,48 +119,28 @@ export function ActionModal< Item >( { ); } -export function ActionWithModal< Item >( { - action, - items, - ActionTrigger, - isBusy, -}: ActionWithModalProps< Item > ) { - const [ isModalOpen, setIsModalOpen ] = useState( false ); - const actionTriggerProps = { - action, - onClick: () => { - setIsModalOpen( true ); - }, - items, - isBusy, - }; - return ( - <> - - { isModalOpen && ( - setIsModalOpen( false ) } - /> - ) } - - ); -} - export function ActionsMenuGroup< Item >( { actions, item, + registry, + setActiveModalAction, }: ActionsMenuGroupProps< Item > ) { - const registry = useRegistry(); return ( - + { actions.map( ( action ) => ( + { + if ( 'RenderModal' in action ) { + setActiveModalAction( action ); + return; + } + action.callback( [ item ], { registry } ); + } } + items={ [ item ] } + /> + ) ) } ); } @@ -210,6 +179,7 @@ export default function ItemActions< Item >( { item={ item } actions={ eligibleActions } isSmall + registry={ registry } /> ); } @@ -239,7 +209,11 @@ export default function ItemActions< Item >( { actions={ primaryActions } registry={ registry } /> - + ); } @@ -248,23 +222,41 @@ function CompactItemActions< Item >( { item, actions, isSmall, + registry, }: CompactItemActionsProps< Item > ) { + const [ activeModalAction, setActiveModalAction ] = useState( + null as ActionModalType< Item > | null + ); return ( - + + } + placement="bottom-end" + > + - } - placement="bottom-end" - > - - + + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } + ); } @@ -273,45 +265,33 @@ function PrimaryActions< Item >( { actions, registry, }: PrimaryActionsProps< Item > ) { + const [ activeModalAction, setActiveModalAction ] = useState( null as any ); if ( ! Array.isArray( actions ) || actions.length === 0 ) { return null; } return ( - - ); -} - -function ActionsList< Item >( { - item, - actions, - registry, - ActionTrigger, -}: ActionsListProps< Item > ) { - return actions.map( ( action ) => { - if ( 'RenderModal' in action ) { - return ( - + { actions.map( ( action ) => ( + { + if ( 'RenderModal' in action ) { + setActiveModalAction( action ); + return; + } + action.callback( [ item ], { registry } ); + } } items={ [ item ] } - ActionTrigger={ ActionTrigger } /> - ); - } - return ( - { - action.callback( [ item ], { registry } ); - } } - items={ [ item ] } - /> - ); - } ); + ) ) } + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } + + ); } diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index d400cc62741699..62d813c1485af8 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -40,6 +40,7 @@ import type { NormalizedField, ViewList as ViewListType, ViewListProps, + ActionModal as ActionModalType, } from '../../types'; interface ListViewItemProps< Item > { @@ -154,7 +155,11 @@ function ListItem< Item >( { const labelId = `${ idPrefix }-label`; const descriptionId = `${ idPrefix }-description`; + const registry = useRegistry(); const [ isHovered, setIsHovered ] = useState( false ); + const [ activeModalAction, setActiveModalAction ] = useState( + null as ActionModalType< Item > | null + ); const handleHover: React.MouseEventHandler = ( { type } ) => { const isHover = type === 'mouseenter'; setIsHovered( isHover ); @@ -233,8 +238,17 @@ function ListItem< Item >( { + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } ) } diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index ab11b5e318b5a6..bfdddb4a5a062d 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useRegistry, useSelect } from '@wordpress/data'; import { useState, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { @@ -21,7 +21,7 @@ import { usePostActions } from './actions'; const { Menu, kebabCase } = unlock( componentsPrivateApis ); export default function PostActions( { postType, postId, onActionPerformed } ) { - const [ isActionsMenuOpen, setIsActionsMenuOpen ] = useState( false ); + const [ activeModalAction, setActiveModalAction ] = useState( null ); const { item, permissions } = useSelect( ( select ) => { const { getEditedEntityRecord, getEntityRecordPermissions } = @@ -54,32 +54,34 @@ export default function PostActions( { postType, postId, onActionPerformed } ) { }, [ allActions, itemWithPermissions ] ); return ( - - setIsActionsMenuOpen( ! isActionsMenuOpen ) - } + <> + + } + placement="bottom-end" + > + - } - onOpenChange={ setIsActionsMenuOpen } - placement="bottom-end" - > - { - setIsActionsMenuOpen( false ); - } } - /> - + + { !! activeModalAction && ( + setActiveModalAction( null ) } + /> + ) } + ); } @@ -88,78 +90,51 @@ export default function PostActions( { postType, postId, onActionPerformed } ) { // and the dataviews package should not be using the editor packages directly, // so duplicating the code here seems like the least bad option. -// Copied as is from packages/dataviews/src/item-actions.js function DropdownMenuItemTrigger( { action, onClick, items } ) { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - + { label } ); } -// Copied as is from packages/dataviews/src/item-actions.js -// With an added onClose prop. -function ActionWithModal( { action, item, ActionTrigger, onClose } ) { - const [ isModalOpen, setIsModalOpen ] = useState( false ); - const actionTriggerProps = { - action, - onClick: () => setIsModalOpen( true ), - items: [ item ], - }; - const { RenderModal, hideModalHeader } = action; +export function ActionModal( { action, items, closeModal } ) { + const label = + typeof action.label === 'string' ? action.label : action.label( items ); return ( - <> - - { isModalOpen && ( - { - setIsModalOpen( false ); - } } - overlayClassName={ `editor-action-modal editor-action-modal__${ kebabCase( - action.id - ) }` } - focusOnMount="firstContentElement" - size="medium" - > - { - setIsModalOpen( false ); - onClose(); - } } - /> - - ) } - + {} ) } + focusOnMount="firstContentElement" + size="medium" + overlayClassName={ `editor-action-modal editor-action-modal__${ kebabCase( + action.id + ) }` } + > + + ); } -// Copied as is from packages/dataviews/src/item-actions.js -// With an added onClose prop. -function ActionsDropdownMenuGroup( { actions, item, onClose } ) { +function ActionsDropdownMenuGroup( { actions, item, setActiveModalAction } ) { + const registry = useRegistry(); return ( { actions.map( ( action ) => { - if ( action.RenderModal ) { - return ( - - ); - } return ( action.callback( [ item ] ) } + onClick={ () => { + if ( 'RenderModal' in action ) { + setActiveModalAction( action ); + return; + } + action.callback( [ item ], { registry } ); + } } items={ [ item ] } /> ); From 91b130b2db14028768e8e85fdf83ac8e5cb0b5b8 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 9 Dec 2024 14:55:49 +0200 Subject: [PATCH 07/50] [Block Library]: Update the relationship of `No results` block to `ancestor` (#48348) Co-authored-by: ntsekouras Co-authored-by: Mamaduka Co-authored-by: gziolo Co-authored-by: webmandesign --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/query-no-results/block.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 71ad6359adca0a..7800eae6c7f2ea 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -724,7 +724,7 @@ Contains the block elements used to render content when no query results are fou - **Name:** core/query-no-results - **Category:** theme -- **Parent:** core/query +- **Ancestor:** core/query - **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ ## Pagination diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json index 2f656594afa306..c7d3ff500e0f43 100644 --- a/packages/block-library/src/query-no-results/block.json +++ b/packages/block-library/src/query-no-results/block.json @@ -5,7 +5,7 @@ "title": "No results", "category": "theme", "description": "Contains the block elements used to render content when no query results are found.", - "parent": [ "core/query" ], + "ancestor": [ "core/query" ], "textdomain": "default", "usesContext": [ "queryId", "query" ], "example": { From 01543397474a600dc1b75f309071d03d6103d4f5 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Mon, 9 Dec 2024 10:14:12 -0300 Subject: [PATCH 08/50] Stylebook: render overview colors in 4 columns (#67597) * render overview colors in 4 columns * use templateColums instead of colums to enable responsive columns * use templateColumns instead of columns * tweak CSS Co-authored-by: matiasbenedetto Co-authored-by: tellthemachines Co-authored-by: jasmussen --- .../components/style-book/color-examples.tsx | 24 ++++++++++++++++--- .../src/components/style-book/constants.ts | 1 - .../src/components/style-book/examples.tsx | 7 +++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/edit-site/src/components/style-book/color-examples.tsx b/packages/edit-site/src/components/style-book/color-examples.tsx index 97bdeecee32d3d..bdc7bc7936bc17 100644 --- a/packages/edit-site/src/components/style-book/color-examples.tsx +++ b/packages/edit-site/src/components/style-book/color-examples.tsx @@ -18,13 +18,25 @@ import { */ import type { Color, Gradient } from './types'; -const ColorExamples = ( { colors, type } ): JSX.Element | null => { +type Props = { + colors: Color[] | Gradient[]; + type: 'colors' | 'gradients'; + templateColumns?: string | number; + itemHeight?: string; +}; + +const ColorExamples = ( { + colors, + type, + templateColumns = '1fr 1fr', + itemHeight = '52px', +}: Props ): JSX.Element | null => { if ( ! colors ) { return null; } return ( - + { colors.map( ( color: Color | Gradient ) => { const className = type === 'gradients' @@ -35,7 +47,13 @@ const ColorExamples = ( { colors, type } ): JSX.Element | null => { className ); - return ; + return ( + + ); } ) } ); diff --git a/packages/edit-site/src/components/style-book/constants.ts b/packages/edit-site/src/components/style-book/constants.ts index 0cbbaec07086db..401d532b98cbb7 100644 --- a/packages/edit-site/src/components/style-book/constants.ts +++ b/packages/edit-site/src/components/style-book/constants.ts @@ -220,7 +220,6 @@ export const STYLE_BOOK_IFRAME_STYLES = ` } .edit-site-style-book__color-example { - height: 32px; border: 1px solid color-mix( in srgb, currentColor 10%, transparent ); } diff --git a/packages/edit-site/src/components/style-book/examples.tsx b/packages/edit-site/src/components/style-book/examples.tsx index cb7b6afcb422ce..b585d556341f8c 100644 --- a/packages/edit-site/src/components/style-book/examples.tsx +++ b/packages/edit-site/src/components/style-book/examples.tsx @@ -89,7 +89,12 @@ function getOverviewBlockExamples( title: __( 'Colors' ), category: 'overview', content: ( - + ), }; From e7724382e790db993682263121e917120d4daa68 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:00:48 +0200 Subject: [PATCH 09/50] Navigation: Fix active item hover color (#67732) * Navigation: Fix active item hover color * Add CHANGELOG entry * Fix duplicate Enhancements sections Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/CHANGELOG.md | 13 +++++-------- .../src/navigation/styles/navigation-styles.tsx | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2d2761924fb633..047d190ee3c5d8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,11 @@ - `GradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) - `CustomGradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) - `RangeControl`: Update the design of the range control marks ([#67611](https://github.com/WordPress/gutenberg/pull/67611)) +- `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). +- `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `MenuItem`: Increase height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `MenuItemsChoice`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). +- `Navigation`: Fix active item hover color ([#67732](https://github.com/WordPress/gutenberg/pull/67732)). ### Deprecations @@ -22,14 +27,6 @@ - `FormFileUpload`: Deprecate 36px default size ([#67438](https://github.com/WordPress/gutenberg/pull/67438)). - `FormTokenField`: Deprecate 36px default size ([#67454](https://github.com/WordPress/gutenberg/pull/67454)). - -### Enhancements - -- `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). -- `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). -- `MenuItem`: Increase height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). -- `MenuItemsChoice`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). - ### Experimental - `Menu`: throw when subcomponents are not rendered inside top level `Menu` ([#67411](https://github.com/WordPress/gutenberg/pull/67411)). diff --git a/packages/components/src/navigation/styles/navigation-styles.tsx b/packages/components/src/navigation/styles/navigation-styles.tsx index 580c0eef4dba8f..71dbccd7717c49 100644 --- a/packages/components/src/navigation/styles/navigation-styles.tsx +++ b/packages/components/src/navigation/styles/navigation-styles.tsx @@ -137,6 +137,7 @@ export const ItemBaseUI = styled.li` color: ${ COLORS.white }; > button, + .components-button:hover, > a { color: ${ COLORS.white }; opacity: 1; From 896b316bc774f0b2da4b4b916196fba4877945a0 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 9 Dec 2024 23:09:46 +0900 Subject: [PATCH 10/50] Components: Deprecate `COLORS.white` (#67649) * Components: Deprecate `COLORS.white` * Add changelogs * Update snapshot Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 3 ++ packages/components/src/menu/styles.ts | 2 +- .../navigation/styles/navigation-styles.tsx | 4 +- .../test/__snapshots__/index.tsx.snap | 44 +++++++++---------- .../styles.ts | 12 ++--- .../toggle-group-control/styles.ts | 2 +- .../components/src/utils/colors-values.js | 3 ++ .../components/src/utils/config-values.js | 1 - 8 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 047d190ee3c5d8..249d4f3f65b936 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,9 @@ - `GradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) - `CustomGradientPicker`: Add `enableAlpha` prop ([#66974](https://github.com/WordPress/gutenberg/pull/66974)) +- `Menu`: Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). +- `Navigation` (deprecated): Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). +- `ToggleGroupControl`: Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). - `RangeControl`: Update the design of the range control marks ([#67611](https://github.com/WordPress/gutenberg/pull/67611)) - `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). - `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). diff --git a/packages/components/src/menu/styles.ts b/packages/components/src/menu/styles.ts index 0e0752bf24cd10..cda5c7321f38b4 100644 --- a/packages/components/src/menu/styles.ts +++ b/packages/components/src/menu/styles.ts @@ -201,7 +201,7 @@ const baseItem = css` [aria-disabled='true'] ) { background-color: ${ COLORS.theme.accent }; - color: ${ COLORS.white }; + color: ${ COLORS.theme.accentInverted }; } /* Keyboard focus (focus-visible) */ diff --git a/packages/components/src/navigation/styles/navigation-styles.tsx b/packages/components/src/navigation/styles/navigation-styles.tsx index 71dbccd7717c49..aa0976a9a0f277 100644 --- a/packages/components/src/navigation/styles/navigation-styles.tsx +++ b/packages/components/src/navigation/styles/navigation-styles.tsx @@ -134,12 +134,12 @@ export const ItemBaseUI = styled.li` &.is-active { background-color: ${ COLORS.theme.accent }; - color: ${ COLORS.white }; + color: ${ COLORS.theme.accentInverted }; > button, .components-button:hover, > a { - color: ${ COLORS.white }; + color: ${ COLORS.theme.accentInverted }; opacity: 1; } } diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index f344cd6ba16528..91e9f291ddf018 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -72,7 +72,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -134,7 +134,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -158,12 +158,12 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; padding-right: 0; - color: #fff; + color: var(--wp-components-color-foreground-inverted, #fff); } @media not ( prefers-reduced-motion ) { @@ -183,7 +183,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-12:active { @@ -211,7 +211,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -235,7 +235,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; @@ -259,7 +259,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = } .emotion-17:active { - background: #fff; + background: var(--wp-components-color-background, #fff); }
@@ -437,7 +437,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -499,7 +499,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -542,7 +542,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-13 { @@ -706,7 +706,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -768,7 +768,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -792,12 +792,12 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; padding-right: 0; - color: #fff; + color: var(--wp-components-color-foreground-inverted, #fff); } @media not ( prefers-reduced-motion ) { @@ -817,7 +817,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-12:active { @@ -845,7 +845,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -869,7 +869,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] user-select: none; width: 100%; z-index: 2; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); height: 32px; aspect-ratio: 1; padding-left: 0; @@ -893,7 +893,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] } .emotion-17:active { - background: #fff; + background: var(--wp-components-color-background, #fff); }
@@ -1065,7 +1065,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio content: ''; position: absolute; pointer-events: none; - background: #1e1e1e; + background: var(--wp-components-color-foreground, #1e1e1e); outline: 2px solid transparent; outline-offset: -3px; --antialiasing-factor: 100; @@ -1127,7 +1127,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio background: transparent; border: none; border-radius: 1px; - color: #757575; + color: var(--wp-components-color-gray-700, #757575); fill: currentColor; cursor: pointer; display: -webkit-box; @@ -1170,7 +1170,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio } .emotion-12:active { - background: #fff; + background: var(--wp-components-color-background, #fff); } .emotion-13 { diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts index c0248f9b3f7f22..a53eced1219db4 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts @@ -38,7 +38,7 @@ export const buttonView = ( { background: transparent; border: none; border-radius: ${ CONFIG.radiusXSmall }; - color: ${ COLORS.gray[ 700 ] }; + color: ${ COLORS.theme.gray[ 700 ] }; fill: currentColor; cursor: pointer; display: flex; @@ -70,7 +70,7 @@ export const buttonView = ( { } &:active { - background: ${ CONFIG.controlBackgroundColor }; + background: ${ COLORS.ui.background }; } ${ isDeselectable && deselectable } @@ -79,7 +79,7 @@ export const buttonView = ( { `; const pressed = css` - color: ${ COLORS.white }; + color: ${ COLORS.theme.foregroundInverted }; &:active { background: transparent; @@ -87,11 +87,11 @@ const pressed = css` `; const deselectable = css` - color: ${ COLORS.gray[ 900 ] }; + color: ${ COLORS.theme.foreground }; &:focus { box-shadow: - inset 0 0 0 1px ${ COLORS.white }, + inset 0 0 0 1px ${ COLORS.ui.background }, 0 0 0 ${ CONFIG.borderWidthFocus } ${ COLORS.theme.accent }; outline: 2px solid transparent; } @@ -112,7 +112,7 @@ const isIconStyles = ( { }; return css` - color: ${ COLORS.gray[ 900 ] }; + color: ${ COLORS.theme.foreground }; height: ${ iconButtonSizes[ size ] }; aspect-ratio: 1; padding-left: 0; diff --git a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts index bb6efe476b2b2c..8376b66a5a86cf 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts @@ -39,7 +39,7 @@ export const toggleGroupControl = ( { content: ''; position: absolute; pointer-events: none; - background: ${ COLORS.gray[ 900 ] }; + background: ${ COLORS.theme.foreground }; // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; diff --git a/packages/components/src/utils/colors-values.js b/packages/components/src/utils/colors-values.js index 1ad528d13f0108..439d464f1460b1 100644 --- a/packages/components/src/utils/colors-values.js +++ b/packages/components/src/utils/colors-values.js @@ -75,6 +75,9 @@ export const COLORS = Object.freeze( { * @deprecated Use semantic aliases in `COLORS.ui` or theme-ready variables in `COLORS.theme.gray`. */ gray: GRAY, // TODO: Stop exporting this when everything is migrated to `theme` or `ui` + /** + * @deprecated Prefer theme-ready variables in `COLORS.theme`. + */ white, alert: ALERT, /** diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index 1bc3945f9b3b16..13b704540e9c4c 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -12,7 +12,6 @@ const CONTROL_PROPS = { controlPaddingXSmall: 8, controlPaddingXLarge: 12 * 1.3334, // TODO: Deprecate - controlBackgroundColor: COLORS.white, controlBoxShadowFocus: `0 0 0 0.5px ${ COLORS.theme.accent }`, controlHeight: CONTROL_HEIGHT, controlHeightXSmall: `calc( ${ CONTROL_HEIGHT } * 0.6 )`, From 9da58a749e2958f65c51352afd2f469379057fc4 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 9 Dec 2024 18:38:42 +0400 Subject: [PATCH 11/50] Data: Expose 'useSelect' warning to third-party consumers (#67735) Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: gziolo Co-authored-by: tyxla --- .../block-editor/src/hooks/test/font-size.js | 27 ++++++++++--------- .../src/hooks/test/use-query-select.js | 12 +++++++++ .../data/src/components/use-select/index.js | 2 +- .../src/components/use-select/test/index.js | 13 +++++++++ .../src/components/with-select/test/index.js | 13 +++++++++ 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/hooks/test/font-size.js b/packages/block-editor/src/hooks/test/font-size.js index 11cd024bf8a285..dc3fc9100e4759 100644 --- a/packages/block-editor/src/hooks/test/font-size.js +++ b/packages/block-editor/src/hooks/test/font-size.js @@ -19,6 +19,15 @@ import { import _fontSize from '../font-size'; const noop = () => {}; +const EMPTY_ARRAY = []; +const EMPTY_OBJECT = {}; +const fontSizes = [ + { + name: 'A larger font', + size: '32px', + slug: 'larger', + }, +]; function addUseSettingFilter( callback ) { addFilter( @@ -55,13 +64,7 @@ describe( 'useBlockProps', () => { registerBlockType( blockSettings.name, blockSettings ); addUseSettingFilter( ( result, path ) => { if ( 'typography.fontSizes' === path ) { - return [ - { - name: 'A larger font', - size: '32px', - slug: 'larger', - }, - ]; + return fontSizes; } if ( 'typography.fluid' === path ) { @@ -69,7 +72,7 @@ describe( 'useBlockProps', () => { } if ( 'layout' === path ) { - return {}; + return EMPTY_OBJECT; } return result; @@ -95,7 +98,7 @@ describe( 'useBlockProps', () => { registerBlockType( blockSettings.name, blockSettings ); addUseSettingFilter( ( result, path ) => { if ( 'typography.fontSizes' === path ) { - return []; + return EMPTY_ARRAY; } if ( 'typography.fluid' === path ) { @@ -103,7 +106,7 @@ describe( 'useBlockProps', () => { } if ( 'layout' === path ) { - return {}; + return EMPTY_OBJECT; } return result; @@ -132,7 +135,7 @@ describe( 'useBlockProps', () => { registerBlockType( blockSettings.name, blockSettings ); addUseSettingFilter( ( result, path ) => { if ( 'typography.fontSizes' === path ) { - return []; + return EMPTY_ARRAY; } if ( 'typography.fluid' === path ) { @@ -140,7 +143,7 @@ describe( 'useBlockProps', () => { } if ( 'layout' === path ) { - return {}; + return EMPTY_OBJECT; } return result; diff --git a/packages/core-data/src/hooks/test/use-query-select.js b/packages/core-data/src/hooks/test/use-query-select.js index 894851f79a9c78..873b897fff4e7d 100644 --- a/packages/core-data/src/hooks/test/use-query-select.js +++ b/packages/core-data/src/hooks/test/use-query-select.js @@ -17,9 +17,16 @@ import { render, screen, waitFor } from '@testing-library/react'; */ import useQuerySelect from '../use-query-select'; +/* eslint-disable @wordpress/wp-global-usage */ describe( 'useQuerySelect', () => { + const initialScriptDebug = globalThis.SCRIPT_DEBUG; let registry; + beforeAll( () => { + // Do not run hook in development mode; it will call `mapSelect` an extra time. + globalThis.SCRIPT_DEBUG = false; + } ); + beforeEach( () => { registry = createRegistry(); registry.registerStore( 'testStore', { @@ -31,6 +38,10 @@ describe( 'useQuerySelect', () => { } ); } ); + afterAll( () => { + globalThis.SCRIPT_DEBUG = initialScriptDebug; + } ); + const getTestComponent = ( mapSelectSpy, dependencyKey ) => ( props ) => { const dependencies = props[ dependencyKey ]; mapSelectSpy.mockImplementation( ( select ) => ( { @@ -188,3 +199,4 @@ describe( 'useQuerySelect', () => { ); } ); } ); +/* eslint-enable @wordpress/wp-global-usage */ diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 6931743151773c..ea4869bb4e7a92 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -174,7 +174,7 @@ function Store( registry, suspense ) { listeningStores ); - if ( process.env.NODE_ENV === 'development' ) { + if ( globalThis.SCRIPT_DEBUG ) { if ( ! didWarnUnstableReference ) { const secondMapResult = mapSelect( select, registry ); if ( ! isShallowEqual( mapResult, secondMapResult ) ) { diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 3320f7d4638a59..6bb47d3c68e9ed 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -32,12 +32,24 @@ function counterStore( initialCount = 0, step = 1 ) { }; } +/* eslint-disable @wordpress/wp-global-usage */ describe( 'useSelect', () => { + const initialScriptDebug = globalThis.SCRIPT_DEBUG; let registry; + + beforeAll( () => { + // Do not run hook in development mode; it will call `mapSelect` an extra time. + globalThis.SCRIPT_DEBUG = false; + } ); + beforeEach( () => { registry = createRegistry(); } ); + afterAll( () => { + globalThis.SCRIPT_DEBUG = initialScriptDebug; + } ); + it( 'passes the relevant data to the component', () => { registry.registerStore( 'testStore', { reducer: () => ( { foo: 'bar' } ), @@ -1257,3 +1269,4 @@ describe( 'useSelect', () => { } ); } ); } ); +/* eslint-enable @wordpress/wp-global-usage */ diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index fe798354cba20d..9e01bb17cbb7e1 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -18,7 +18,19 @@ import withDispatch from '../../with-dispatch'; import { createRegistry } from '../../../registry'; import { RegistryProvider } from '../../registry-provider'; +/* eslint-disable @wordpress/wp-global-usage */ describe( 'withSelect', () => { + const initialScriptDebug = globalThis.SCRIPT_DEBUG; + + beforeAll( () => { + // Do not run HOC in development mode; it will call `mapSelect` an extra time. + globalThis.SCRIPT_DEBUG = false; + } ); + + afterAll( () => { + globalThis.SCRIPT_DEBUG = initialScriptDebug; + } ); + it( 'passes the relevant data to the component', () => { const registry = createRegistry(); registry.registerStore( 'reactReducer', { @@ -615,3 +627,4 @@ describe( 'withSelect', () => { expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'second' ); } ); } ); +/* eslint-enable @wordpress/wp-global-usage */ From e85937ff6868a4b35856845755636c39dbbc555a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=A4gy?= Date: Mon, 9 Dec 2024 15:51:18 +0100 Subject: [PATCH 12/50] Feature: Add `navigation.isLoading` state to core/router store (#67680) --- packages/interactivity-router/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts index 8a46d4ce350113..0c10e896ce1ef5 100644 --- a/packages/interactivity-router/src/index.ts +++ b/packages/interactivity-router/src/index.ts @@ -223,6 +223,7 @@ interface Store { state: { url: string; navigation: { + isLoading: boolean; hasStarted: boolean; hasFinished: boolean; }; @@ -237,6 +238,7 @@ export const { state, actions } = store< Store >( 'core/router', { state: { url: window.location.href, navigation: { + isLoading: false, hasStarted: false, hasFinished: false, }, @@ -289,6 +291,7 @@ export const { state, actions } = store< Store >( 'core/router', { return; } + navigation.isLoading = true; if ( loadingAnimation ) { navigation.hasStarted = true; navigation.hasFinished = false; @@ -328,6 +331,7 @@ export const { state, actions } = store< Store >( 'core/router', { // Update the navigation status once the the new page rendering // has been completed. + navigation.isLoading = false; if ( loadingAnimation ) { navigation.hasStarted = false; navigation.hasFinished = true; From 6b8e47fbd17c0710a4e55d4104dce185fdfac53b Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 9 Dec 2024 07:09:23 -0800 Subject: [PATCH 13/50] TypeScript: Convert factory utils in data package to TS (#67667) * TypeScript: Convert factory utils in data package to TS * Fix docgen build error * Update docs * Revert and fix TS error * Extract and improve signature --- packages/core-data/src/private-selectors.ts | 2 +- packages/data/README.md | 17 +++------ ...{create-selector.js => create-selector.ts} | 4 --- packages/data/src/{factory.js => factory.ts} | 35 +++++++++++++------ 4 files changed, 30 insertions(+), 28 deletions(-) rename packages/data/src/{create-selector.js => create-selector.ts} (55%) rename packages/data/src/{factory.js => factory.ts} (75%) diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 77790512653065..abdf2c837e8ed6 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -92,7 +92,7 @@ export function getEntityRecordPermissions( name: string, id: string ) { - return getEntityRecordsPermissions( state, kind, name, id )[ 0 ]; + return getEntityRecordsPermissions( state, kind, name, [ id ] )[ 0 ]; } /** diff --git a/packages/data/README.md b/packages/data/README.md index b6e0e03b1d8b72..67c01af24bde32 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -418,11 +418,11 @@ When registering a control created with `createRegistryControl` with a store, th _Parameters_ -- _registryControl_ `Function`: Function receiving a registry object and returning a control. +- _registryControl_ `T & { isRegistryControl?: boolean; }`: Function receiving a registry object and returning a control. _Returns_ -- `Function`: Registry control that can be registered with a store. +- Registry control that can be registered with a store. ### createRegistrySelector @@ -471,11 +471,11 @@ with a store. _Parameters_ -- _registrySelector_ `Function`: Function receiving a registry `select` function and returning a state selector. +- _registrySelector_ `( select: ) => Selector`: Function receiving a registry `select` function and returning a state selector. _Returns_ -- `Function`: Registry selector that can be registered with a store. +- `RegistrySelector< Selector >`: Registry selector that can be registered with a store. ### createSelector @@ -485,15 +485,6 @@ _Related_ - The documentation for the `rememo` package from which the `createSelector` function is reexported. -_Parameters_ - -- _selector_ `Function`: Selector function that calculates a value from state and parameters. -- _getDependants_ `Function`: Function that returns an array of "dependant" objects. - -_Returns_ - -- `Function`: A memoized version of `selector` that caches the calculated return values. - ### dispatch Given a store descriptor, returns an object of the store's action creators. Calling an action creator will cause it to be dispatched, updating the state value accordingly. diff --git a/packages/data/src/create-selector.js b/packages/data/src/create-selector.ts similarity index 55% rename from packages/data/src/create-selector.js rename to packages/data/src/create-selector.ts index 069932e8007e28..bfb7a1d283733f 100644 --- a/packages/data/src/create-selector.js +++ b/packages/data/src/create-selector.ts @@ -3,9 +3,5 @@ * and the selector parameters, and recomputes the values only when any of them changes. * * @see The documentation for the `rememo` package from which the `createSelector` function is reexported. - * - * @param {Function} selector Selector function that calculates a value from state and parameters. - * @param {Function} getDependants Function that returns an array of "dependant" objects. - * @return {Function} A memoized version of `selector` that caches the calculated return values. */ export { default as createSelector } from 'rememo'; diff --git a/packages/data/src/factory.js b/packages/data/src/factory.ts similarity index 75% rename from packages/data/src/factory.js rename to packages/data/src/factory.ts index be4ef8cf673c5e..8218fd2cdb07db 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.ts @@ -1,3 +1,14 @@ +/** + * Internal dependencies + */ +import type { select as globalSelect } from './select'; + +type RegistrySelector< Selector extends ( ...args: any[] ) => any > = { + ( ...args: Parameters< Selector > ): ReturnType< Selector >; + isRegistrySelector?: boolean; + registry?: any; +}; + /** * Creates a selector function that takes additional curried argument with the * registry `select` function. While a regular selector has signature @@ -33,17 +44,21 @@ * registry as argument. The registry binding happens automatically when registering the selector * with a store. * - * @param {Function} registrySelector Function receiving a registry `select` - * function and returning a state selector. + * @param registrySelector Function receiving a registry `select` + * function and returning a state selector. * - * @return {Function} Registry selector that can be registered with a store. + * @return Registry selector that can be registered with a store. */ -export function createRegistrySelector( registrySelector ) { +export function createRegistrySelector< + Selector extends ( ...args: any[] ) => any, +>( + registrySelector: ( select: typeof globalSelect ) => Selector +): RegistrySelector< Selector > { const selectorsByRegistry = new WeakMap(); // Create a selector function that is bound to the registry referenced by `selector.registry` // and that has the same API as a regular selector. Binding it in such a way makes it // possible to call the selector directly from another selector. - const wrappedSelector = ( ...args ) => { + const wrappedSelector: RegistrySelector< Selector > = ( ...args ) => { let selector = selectorsByRegistry.get( wrappedSelector.registry ); // We want to make sure the cache persists even when new registry // instances are created. For example patterns create their own editors @@ -60,8 +75,6 @@ export function createRegistrySelector( registrySelector ) { * Flag indicating that the selector is a registry selector that needs the correct registry * reference to be assigned to `selector.registry` to make it work correctly. * be mapped as a registry selector. - * - * @type {boolean} */ wrappedSelector.isRegistrySelector = true; @@ -84,11 +97,13 @@ export function createRegistrySelector( registrySelector ) { * When registering a control created with `createRegistryControl` with a store, the store * knows which calling convention to use when executing the control. * - * @param {Function} registryControl Function receiving a registry object and returning a control. + * @param registryControl Function receiving a registry object and returning a control. * - * @return {Function} Registry control that can be registered with a store. + * @return Registry control that can be registered with a store. */ -export function createRegistryControl( registryControl ) { +export function createRegistryControl< T extends ( ...args: any ) => any >( + registryControl: T & { isRegistryControl?: boolean } +) { registryControl.isRegistryControl = true; return registryControl; From 9ae9ec35ff68f3426d3a75e3c543d80c1f16c3e2 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 9 Dec 2024 16:39:08 +0100 Subject: [PATCH 14/50] DataViews: Fix filters lost when switching layouts (#67740) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-list/index.js | 90 ++++++++++--------- test/e2e/specs/site-editor/page-list.spec.js | 56 ++++++++++++ 2 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 test/e2e/specs/site-editor/page-list.spec.js diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 145a5e8243ac54..a67a505795b3c8 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -13,7 +13,7 @@ import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; import { drawerRight } from '@wordpress/icons'; -import { usePrevious } from '@wordpress/compose'; +import { useEvent, usePrevious } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; /** @@ -112,53 +112,50 @@ function useView( postType ) { }; } ); - const setViewWithUrlUpdate = useCallback( - ( newView ) => { - if ( newView.type === LAYOUT_LIST && ! layout ) { - // Skip updating the layout URL param if - // it is not present and the newView.type is LAYOUT_LIST. - } else if ( newView.type !== layout ) { - history.navigate( - addQueryArgs( path, { - layout: newView.type, - } ) - ); - } + const setViewWithUrlUpdate = useEvent( ( newView ) => { + setView( newView ); - setView( newView ); + if ( isCustom === 'true' && editedEntityRecord?.id ) { + editEntityRecord( + 'postType', + 'wp_dataviews', + editedEntityRecord?.id, + { + content: JSON.stringify( newView ), + } + ); + } - if ( isCustom === 'true' && editedEntityRecord?.id ) { - editEntityRecord( - 'postType', - 'wp_dataviews', - editedEntityRecord?.id, - { - content: JSON.stringify( newView ), - } - ); - } - }, - [ - history, - isCustom, - editEntityRecord, - editedEntityRecord?.id, - layout, - path, - ] - ); + const currentUrlLayout = layout ?? LAYOUT_LIST; + if ( newView.type !== currentUrlLayout ) { + history.navigate( + addQueryArgs( path, { + layout: newView.type, + } ) + ); + } + } ); // When layout URL param changes, update the view type // without affecting any other config. + const onUrlLayoutChange = useEvent( () => { + setView( ( prevView ) => { + const layoutToApply = layout ?? LAYOUT_LIST; + if ( layoutToApply === prevView.type ) { + return prevView; + } + return { + ...prevView, + type: layout ?? LAYOUT_LIST, + }; + } ); + } ); useEffect( () => { - setView( ( prevView ) => ( { - ...prevView, - type: layout ?? LAYOUT_LIST, - } ) ); - }, [ layout ] ); + onUrlLayoutChange(); + }, [ onUrlLayoutChange, layout ] ); // When activeView or isCustom URL parameters change, reset the view. - useEffect( () => { + const onUrlActiveViewChange = useEvent( () => { let newView; if ( isCustom === 'true' ) { newView = getCustomView( editedEntityRecord ); @@ -173,9 +170,18 @@ function useView( postType ) { type, } ); } - }, [ activeView, isCustom, layout, defaultViews, editedEntityRecord ] ); + } ); + useEffect( () => { + onUrlActiveViewChange(); + }, [ + onUrlActiveViewChange, + activeView, + isCustom, + defaultViews, + editedEntityRecord, + ] ); - return [ view, setViewWithUrlUpdate, setViewWithUrlUpdate ]; + return [ view, setViewWithUrlUpdate ]; } const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All but 'trash'. diff --git a/test/e2e/specs/site-editor/page-list.spec.js b/test/e2e/specs/site-editor/page-list.spec.js new file mode 100644 index 00000000000000..fa9cb86cd1d62e --- /dev/null +++ b/test/e2e/specs/site-editor/page-list.spec.js @@ -0,0 +1,56 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Page List', () => { + test.beforeAll( async ( { requestUtils } ) => { + // Activate a theme with permissions to access the site editor. + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.createPage( { + title: 'Privacy Policy', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Sample Page', + status: 'publish', + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + // Go back to the default theme. + await Promise.all( [ + requestUtils.activateTheme( 'twentytwentyone' ), + requestUtils.deleteAllPages(), + ] ); + } ); + + test.beforeEach( async ( { admin, page } ) => { + // Go to the pages page, as it has the list layout enabled by default. + await admin.visitSiteEditor(); + await page.getByRole( 'button', { name: 'Pages' } ).click(); + } ); + + test( 'Persists filter/search when switching layout', async ( { + page, + } ) => { + // Search pages + await page + .getByRole( 'searchbox', { name: 'Search' } ) + .fill( 'Privacy' ); + + // Switch layout + await page.getByRole( 'button', { name: 'Layout' } ).click(); + await page.getByRole( 'menuitemradio', { name: 'Table' } ).click(); + + // Confirm the table is visible + await expect( page.getByRole( 'table' ) ).toContainText( + 'Privacy Policy' + ); + + // The search should still contain the search term + await expect( + page.getByRole( 'searchbox', { name: 'Search' } ) + ).toHaveValue( 'Privacy' ); + } ); +} ); From d03eeae0d48b35fb3e5e05a90bbc7b956a6b72f5 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:15:04 +0000 Subject: [PATCH 15/50] wp-env: Add phpMyAdmin support (#67588) * wp-env: Add phpMyAdmin support Defines two new docker-compose services: `phpmyadmin` and `tests-phpmyadmin`. These are off by default. They can be individually turned on by: - Specifying a port in any enviroment's config via key `phpmyadminPort` (see bottom of commit message for example). - By setting environment variable `WP_ENV_PHPMYADMIN_PORT` and/or `WP_ENV_TESTS_PHPMYADMIN_PORT`. * Opt into the phpMyAdmin service in the Gutenberg environment * wp-env: start: refactor computation of command output - Refactor repeating logic into getPublicDockerPort - Remove trailing newlines from dockerCompose output - Refactor evaluation of spinner.prefixText so as to clearly reveal newlines. { env: { development: { ... phpmyadminPort: 9000 }, tests: { ... } } } --------- Co-authored-by: Riad Benguella --- .wp-env.json | 3 + packages/env/CHANGELOG.md | 4 + packages/env/README.md | 2 +- .../env/lib/build-docker-compose-config.js | 27 +++++++ packages/env/lib/commands/start.js | 76 +++++++++++++++---- .../get-config-from-environment-vars.js | 7 ++ packages/env/lib/config/parse-config.js | 27 +++++-- .../__snapshots__/config-integration.js.snap | 8 ++ packages/env/lib/config/test/parse-config.js | 54 +++++++++++++ packages/env/lib/validate-run-container.js | 1 + schemas/json/wp-env.json | 7 +- 11 files changed, 190 insertions(+), 26 deletions(-) diff --git a/.wp-env.json b/.wp-env.json index 05ea05b2809f9c..d368f3ea1c72a6 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -4,6 +4,9 @@ "plugins": [ "." ], "themes": [ "./test/emptytheme" ], "env": { + "development": { + "phpmyadminPort": 9000 + }, "tests": { "mappings": { "wp-content/plugins/gutenberg": ".", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 651d6b285e1bd6..6ee37efdf850c9 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Add phpMyAdmin as an optional service. Enabled via the new `phpmyadminPort` environment config, as well as env vars `WP_ENV_PHPMYADMIN_PORT` and `WP_ENV_TESTS_PHPMYADMIN_PORT`. + ## 10.13.0 (2024-11-27) ## 10.12.0 (2024-11-16) diff --git a/packages/env/README.md b/packages/env/README.md index 35b00fe83e8f4d..af037f5ee80ab6 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -349,7 +349,7 @@ containers. Positionals: container The Docker service to run the command on. [string] [required] [choices: "mysql", "tests-mysql", "wordpress", - "tests-wordpress", "cli", "tests-cli", "composer", "phpunit"] + "tests-wordpress", "cli", "tests-cli", "composer", "phpmyadmin"] command The command to run. [required] Options: diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index a394537e360872..a1a4f68256b688 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -174,6 +174,13 @@ module.exports = function buildDockerComposeConfig( config ) { config.env.tests.mysqlPort ?? '' }}:3306`; + const developmentPhpmyadminPorts = `\${WP_ENV_PHPMYADMIN_PORT:-${ + config.env.development.phpmyadminPort ?? '' + }}:80`; + const testsPhpmyadminPorts = `\${WP_ENV_TESTS_PHPMYADMIN_PORT:-${ + config.env.tests.phpmyadminPort ?? '' + }}:80`; + return { services: { mysql: { @@ -266,6 +273,26 @@ module.exports = function buildDockerComposeConfig( config ) { }, extra_hosts: [ 'host.docker.internal:host-gateway' ], }, + phpmyadmin: { + image: 'phpmyadmin', + ports: [ developmentPhpmyadminPorts ], + environment: { + PMA_PORT: 3306, + PMA_HOST: 'mysql', + PMA_USER: 'root', + PMA_PASSWORD: 'password', + }, + }, + 'tests-phpmyadmin': { + image: 'phpmyadmin', + ports: [ testsPhpmyadminPorts ], + environment: { + PMA_PORT: 3306, + PMA_HOST: 'tests-mysql', + PMA_USER: 'root', + PMA_PASSWORD: 'password', + }, + }, }, volumes: { ...( ! config.env.development.coreSource && { wordpress: {} } ), diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 4203ac74632287..24d57d71fd3c62 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -180,6 +180,24 @@ module.exports = async function start( { } ); + if ( config.env.development.phpmyadminPort ) { + await dockerCompose.upOne( 'phpmyadmin', { + ...dockerComposeConfig, + commandOptions: shouldConfigureWp + ? [ '--build', '--force-recreate' ] + : [], + } ); + } + + if ( config.env.tests.phpmyadminPort ) { + await dockerCompose.upOne( 'tests-phpmyadmin', { + ...dockerComposeConfig, + commandOptions: shouldConfigureWp + ? [ '--build', '--force-recreate' ] + : [], + } ); + } + // Make sure we've consumed the custom CLI dockerfile. if ( shouldConfigureWp ) { await dockerCompose.buildOne( [ 'cli' ], { ...dockerComposeConfig } ); @@ -225,35 +243,61 @@ module.exports = async function start( { const siteUrl = config.env.development.config.WP_SITEURL; const testsSiteUrl = config.env.tests.config.WP_SITEURL; - const { out: mySQLAddress } = await dockerCompose.port( + const mySQLPort = await getPublicDockerPort( 'mysql', 3306, dockerComposeConfig ); - const mySQLPort = mySQLAddress.split( ':' ).pop(); - const { out: testsMySQLAddress } = await dockerCompose.port( + const testsMySQLPort = await getPublicDockerPort( 'tests-mysql', 3306, dockerComposeConfig ); - const testsMySQLPort = testsMySQLAddress.split( ':' ).pop(); - - spinner.prefixText = 'WordPress development site started' - .concat( siteUrl ? ` at ${ siteUrl }` : '.' ) - .concat( '\n' ) - .concat( 'WordPress test site started' ) - .concat( testsSiteUrl ? ` at ${ testsSiteUrl }` : '.' ) - .concat( '\n' ) - .concat( `MySQL is listening on port ${ mySQLPort }` ) - .concat( - `MySQL for automated testing is listening on port ${ testsMySQLPort }` - ) - .concat( '\n' ); + const phpmyadminPort = config.env.development.phpmyadminPort + ? await getPublicDockerPort( 'phpmyadmin', 80, dockerComposeConfig ) + : null; + + const testsPhpmyadminPort = config.env.tests.phpmyadminPort + ? await getPublicDockerPort( + 'tests-phpmyadmin', + 80, + dockerComposeConfig + ) + : null; + + spinner.prefixText = [ + 'WordPress development site started' + + ( siteUrl ? ` at ${ siteUrl }` : '.' ), + 'WordPress test site started' + + ( testsSiteUrl ? ` at ${ testsSiteUrl }` : '.' ), + `MySQL is listening on port ${ mySQLPort }`, + `MySQL for automated testing is listening on port ${ testsMySQLPort }`, + phpmyadminPort && + `phpMyAdmin started at http://localhost:${ phpmyadminPort }`, + testsPhpmyadminPort && + `phpMyAdmin for automated testing started at http://localhost:${ testsPhpmyadminPort }`, + ] + .filter( Boolean ) + .join( '\n' ); + spinner.prefixText += '\n\n'; spinner.text = 'Done!'; }; +async function getPublicDockerPort( + service, + containerPort, + dockerComposeConfig +) { + const { out: address } = await dockerCompose.port( + service, + containerPort, + dockerComposeConfig + ); + return address.split( ':' ).pop().trim(); +} + /** * Checks for legacy installs and provides * the user the option to delete them. diff --git a/packages/env/lib/config/get-config-from-environment-vars.js b/packages/env/lib/config/get-config-from-environment-vars.js index beaf721ea1c0c1..618b5fff257920 100644 --- a/packages/env/lib/config/get-config-from-environment-vars.js +++ b/packages/env/lib/config/get-config-from-environment-vars.js @@ -20,6 +20,7 @@ const { checkPort, checkVersion, checkString } = require( './validate-config' ); * @property {?number} mysqlPort An override for the development environment's MySQL port. * @property {?number} testsPort An override for the testing environment's port. * @property {?number} testsMysqlPort An override for the testing environment's MySQL port. + * @property {?number} phpmyadminPort An override for the development environment's phpMyAdmin port. * @property {?WPSource} coreSource An override for all environment's coreSource. * @property {?string} phpVersion An override for all environment's PHP version. * @property {?Object.} lifecycleScripts An override for various lifecycle scripts. @@ -40,6 +41,12 @@ module.exports = function getConfigFromEnvironmentVars( cacheDirectoryPath ) { testsMysqlPort: getPortFromEnvironmentVariable( 'WP_ENV_TESTS_MYSQL_PORT' ), + phpmyadminPort: getPortFromEnvironmentVariable( + 'WP_ENV_PHPMYADMIN_PORT' + ), + testsPhpmyadminPort: getPortFromEnvironmentVariable( + 'WP_ENV_TESTS_PHPMYADMIN_PORT' + ), lifecycleScripts: getLifecycleScriptOverrides(), }; diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index 1fc7e949251490..bddd7bc72aaee0 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -46,14 +46,15 @@ const mergeConfigs = require( './merge-configs' ); * The environment-specific configuration options. (development/tests/etc) * * @typedef WPEnvironmentConfig - * @property {WPSource} coreSource The WordPress installation to load in the environment. - * @property {WPSource[]} pluginSources Plugins to load in the environment. - * @property {WPSource[]} themeSources Themes to load in the environment. - * @property {number} port The port to use. - * @property {number} mysqlPort The port to use for MySQL. Random if empty. - * @property {Object} config Mapping of wp-config.php constants to their desired values. - * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. - * @property {string|null} phpVersion Version of PHP to use in the environments, of the format 0.0. + * @property {WPSource} coreSource The WordPress installation to load in the environment. + * @property {WPSource[]} pluginSources Plugins to load in the environment. + * @property {WPSource[]} themeSources Themes to load in the environment. + * @property {number} port The port to use. + * @property {number} mysqlPort The port to use for MySQL. Random if empty. + * @property {number} phpmyadminPort The port to use for phpMyAdmin. If empty, disabled phpMyAdmin. + * @property {Object} config Mapping of wp-config.php constants to their desired values. + * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. + * @property {string|null} phpVersion Version of PHP to use in the environments, of the format 0.0. */ /** @@ -87,6 +88,7 @@ const DEFAULT_ENVIRONMENT_CONFIG = { port: 8888, testsPort: 8889, mysqlPort: null, + phpmyadminPort: null, mappings: {}, config: { FS_METHOD: 'direct', @@ -282,6 +284,11 @@ function getEnvironmentVarOverrides( cacheDirectoryPath ) { overrideConfig.env.development.mysqlPort = overrides.mysqlPort; } + if ( overrides.phpmyadminPort ) { + overrideConfig.env.development.phpmyadminPort = + overrides.phpmyadminPort; + } + if ( overrides.testsPort ) { overrideConfig.testsPort = overrides.testsPort; overrideConfig.env.tests.port = overrides.testsPort; @@ -455,6 +462,10 @@ async function parseEnvironmentConfig( parsedConfig.mysqlPort = config.mysqlPort; } + if ( config.phpmyadminPort !== undefined ) { + parsedConfig.phpmyadminPort = config.phpmyadminPort; + } + if ( config.phpVersion !== undefined ) { // Support null as a valid input. if ( config.phpVersion !== null ) { diff --git a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap index 6c3618f4724cb0..6b671a6bc858eb 100644 --- a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap +++ b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap @@ -31,6 +31,7 @@ exports[`Config Integration should load local and override configuration files 1 "mappings": {}, "mysqlPort": 23306, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 999, "themeSources": [], @@ -60,6 +61,7 @@ exports[`Config Integration should load local and override configuration files 1 "mappings": {}, "mysqlPort": 23307, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 456, "themeSources": [], @@ -106,6 +108,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "mappings": {}, "mysqlPort": 13306, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 123, "themeSources": [], @@ -135,6 +138,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "mappings": {}, "mysqlPort": 23307, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 8889, "themeSources": [], @@ -181,6 +185,7 @@ exports[`Config Integration should use default configuration 1`] = ` "mappings": {}, "mysqlPort": null, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 8888, "themeSources": [], @@ -210,6 +215,7 @@ exports[`Config Integration should use default configuration 1`] = ` "mappings": {}, "mysqlPort": null, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 8889, "themeSources": [], @@ -256,6 +262,7 @@ exports[`Config Integration should use environment variables over local and over "mappings": {}, "mysqlPort": 23306, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 12345, "testsPort": 61234, @@ -286,6 +293,7 @@ exports[`Config Integration should use environment variables over local and over "mappings": {}, "mysqlPort": 23307, "phpVersion": null, + "phpmyadminPort": null, "pluginSources": [], "port": 61234, "testsPort": 61234, diff --git a/packages/env/lib/config/test/parse-config.js b/packages/env/lib/config/test/parse-config.js index 38e4db9860cb3c..cc6e2c7a96bbc0 100644 --- a/packages/env/lib/config/test/parse-config.js +++ b/packages/env/lib/config/test/parse-config.js @@ -22,6 +22,7 @@ const DEFAULT_CONFIG = { port: 8888, testsPort: 8889, mysqlPort: null, + phpmyadminPort: null, phpVersion: null, coreSource: { type: 'git', @@ -400,4 +401,57 @@ describe( 'parseConfig', () => { ) ); } ); + + it( 'should parse phpmyadmin configuration for a given environment', async () => { + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === '/test/gutenberg/.wp-env.json' ) { + return { + core: 'WordPress/WordPress#Test', + phpVersion: '1.0', + lifecycleScripts: { + afterStart: 'test', + }, + env: { + development: { + phpmyadminPort: 9001, + }, + }, + }; + } + } ); + + const parsed = await parseConfig( '/test/gutenberg', '/cache' ); + + const expected = { + development: { + ...DEFAULT_CONFIG.env.development, + phpmyadminPort: 9001, + }, + tests: DEFAULT_CONFIG.env.tests, + }; + expect( parsed.env ).toEqual( expected ); + } ); + + it( 'should ignore root-level configuration for phpmyadmin', async () => { + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === '/test/gutenberg/.wp-env.json' ) { + return { + core: 'WordPress/WordPress#Test', + phpVersion: '1.0', + lifecycleScripts: { + afterStart: 'test', + }, + phpmyadminPort: 9001, + }; + } + } ); + + const parsed = await parseConfig( '/test/gutenberg', '/cache' ); + + const expected = { + development: DEFAULT_CONFIG.env.development, + tests: DEFAULT_CONFIG.env.tests, + }; + expect( parsed.env ).toEqual( expected ); + } ); } ); diff --git a/packages/env/lib/validate-run-container.js b/packages/env/lib/validate-run-container.js index 77e68d2a3a4dc7..fbb6670f6c1500 100644 --- a/packages/env/lib/validate-run-container.js +++ b/packages/env/lib/validate-run-container.js @@ -10,6 +10,7 @@ const RUN_CONTAINERS = [ 'tests-wordpress', 'cli', 'tests-cli', + 'phpmyadmin', ]; /** diff --git a/schemas/json/wp-env.json b/schemas/json/wp-env.json index 491d1f8cf73017..8aa604ed41ed1f 100644 --- a/schemas/json/wp-env.json +++ b/schemas/json/wp-env.json @@ -62,6 +62,10 @@ "description": "Mapping of WordPress directories to local directories to be mounted in the WordPress instance.", "type": "object", "default": {} + }, + "phpmyadminPort": { + "description": "The port number to access phpMyAdmin.", + "type": "integer" } } }, @@ -73,7 +77,8 @@ "themes", "port", "config", - "mappings" + "mappings", + "phpmyadminPort" ] } }, From ad0c1d444ae62e67d6a75ecaaeea8f62d23badba Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 9 Dec 2024 21:15:56 +0400 Subject: [PATCH 16/50] Replace remaining custom deep cloning with 'structuredClone' (#67707) * Replace remaining custom deep cloning with 'structuredClone' * Polyfill structuredClone for jsdom Co-authored-by: Mamaduka Co-authored-by: tyxla Co-authored-by: jsnajdr --- .../src/components/global-styles/use-global-styles-output.js | 2 +- packages/block-editor/src/hooks/style.js | 2 +- test/unit/config/global-mocks.js | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 7bdc95d222142d..fabc65d143d1aa 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -624,7 +624,7 @@ function pickStyleKeys( treeToPickFrom ) { // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [ key, - JSON.parse( JSON.stringify( style ) ), + structuredClone( style ), ] ); return Object.fromEntries( clonedEntries ); } diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index db2acd01665b60..5be2b1b3fd40a8 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -245,7 +245,7 @@ export function omitStyle( style, paths, preserveReference = false ) { let newStyle = style; if ( ! preserveReference ) { - newStyle = JSON.parse( JSON.stringify( style ) ); + newStyle = structuredClone( style ); } if ( ! Array.isArray( paths ) ) { diff --git a/test/unit/config/global-mocks.js b/test/unit/config/global-mocks.js index ce64f03b514be8..8db2c180fadf3a 100644 --- a/test/unit/config/global-mocks.js +++ b/test/unit/config/global-mocks.js @@ -3,6 +3,7 @@ */ import { TextDecoder, TextEncoder } from 'node:util'; import { Blob as BlobPolyfill, File as FilePolyfill } from 'node:buffer'; +import 'core-js/stable/structured-clone'; jest.mock( '@wordpress/compose', () => { return { @@ -49,3 +50,6 @@ if ( ! global.TextEncoder ) { // Override jsdom built-ins with native node implementation. global.Blob = BlobPolyfill; global.File = FilePolyfill; + +// Polyfill structuredClone for jsdom. +global.structuredClone = structuredClone; From 95cbcdf5daacccccd181dfa7850e706c5a1e009c Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 9 Dec 2024 11:34:42 -0600 Subject: [PATCH 17/50] Inserter: Should receive focus on open (#67754) * Set default tab to 'blocks' if none passed * Add tests for focusing blocks tab and closing inserter on keypresses --- .../src/components/inserter/menu.js | 2 + .../site-editor/site-editor-inserter.spec.js | 53 +++++++++++++------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index ef260beb85906b..019a37bffdde26 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -82,6 +82,8 @@ function InserterMenu( if ( isZoomOutMode ) { return 'patterns'; } + + return 'blocks'; } const [ selectedTab, setSelectedTab ] = useState( getInitialTab() ); diff --git a/test/e2e/specs/site-editor/site-editor-inserter.spec.js b/test/e2e/specs/site-editor/site-editor-inserter.spec.js index a730367d841bf8..23646576131082 100644 --- a/test/e2e/specs/site-editor/site-editor-inserter.spec.js +++ b/test/e2e/specs/site-editor/site-editor-inserter.spec.js @@ -28,32 +28,51 @@ test.describe( 'Site Editor Inserter', () => { }, } ); - // eslint-disable-next-line playwright/expect-expect test( 'inserter toggle button should toggle global inserter', async ( { InserterUtils, + page, + editor, } ) => { await InserterUtils.openBlockLibrary(); await InserterUtils.closeBlockLibrary(); - } ); - // A test for https://github.com/WordPress/gutenberg/issues/43090. - test( 'should close the inserter when clicking on the toggle button', async ( { - editor, - InserterUtils, - } ) => { - const beforeBlocks = await editor.getBlocks(); + await test.step( 'should open the inserter via enter keypress on toggle button', async () => { + await InserterUtils.inserterButton.focus(); + await page.keyboard.press( 'Enter' ); + await expect( InserterUtils.blockLibrary ).toBeVisible(); + } ); - await InserterUtils.openBlockLibrary(); - await InserterUtils.expectActiveTab( 'Blocks' ); - await InserterUtils.blockLibrary - .getByRole( 'option', { name: 'Buttons' } ) - .click(); + await test.step( 'should set focus to the blocks tab when opening the inserter', async () => { + await expect( + InserterUtils.getBlockLibraryTab( 'Blocks' ) + ).toBeFocused(); + } ); + + await test.step( 'should close the inserter via escape keypress', async () => { + await page.keyboard.press( 'Escape' ); + await expect( InserterUtils.blockLibrary ).toBeHidden(); + } ); - await expect - .poll( editor.getBlocks ) - .toMatchObject( [ ...beforeBlocks, { name: 'core/buttons' } ] ); + await test.step( 'should focus inserter toggle button after closing the inserter via escape keypress', async () => { + await expect( InserterUtils.inserterButton ).toBeFocused(); + } ); - await InserterUtils.closeBlockLibrary(); + // A test for https://github.com/WordPress/gutenberg/issues/43090. + await test.step( 'should close the inserter when clicking on the toggle button', async () => { + const beforeBlocks = await editor.getBlocks(); + + await InserterUtils.openBlockLibrary(); + await InserterUtils.expectActiveTab( 'Blocks' ); + await InserterUtils.blockLibrary + .getByRole( 'option', { name: 'Buttons' } ) + .click(); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ ...beforeBlocks, { name: 'core/buttons' } ] ); + + await InserterUtils.closeBlockLibrary(); + } ); } ); test.describe( 'Inserter Zoom Level UX', () => { From f430cd73ff8eaf58aa9782d50891e8eb50a9ee54 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 9 Dec 2024 18:24:42 +0000 Subject: [PATCH 18/50] Fix: Dataviews remove primary field concept from some classes. (#67689) Co-authored-by: jorgefilipecosta Co-authored-by: youknowriad --- packages/dataviews/src/dataviews-layouts/grid/index.tsx | 8 +++----- packages/dataviews/src/dataviews-layouts/grid/style.scss | 2 +- packages/dataviews/src/dataviews-layouts/list/style.scss | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index 4a016095702026..e10ae7b591af5f 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -82,11 +82,11 @@ function GridItem< Item >( { className: 'dataviews-view-grid__media', } ); - const clickablePrimaryItemProps = getClickableItemProps( { + const clickableTitleItemProps = getClickableItemProps( { item, isItemClickable, onClickItem, - className: 'dataviews-view-grid__primary-field dataviews-title-field', + className: 'dataviews-view-grid__title-field dataviews-title-field', } ); return ( @@ -128,9 +128,7 @@ function GridItem< Item >( { justify="space-between" className="dataviews-view-grid__title-actions" > -
- { renderedTitleField } -
+
{ renderedTitleField }
diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index 70c94653371d16..e9fcb472dc3186 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -15,7 +15,7 @@ padding: $grid-unit-10 0 $grid-unit-05; } - .dataviews-view-grid__primary-field { + .dataviews-view-grid__title-field { min-height: $grid-unit-30; // Preserve layout when there is no ellipsis button display: flex; align-items: center; diff --git a/packages/dataviews/src/dataviews-layouts/list/style.scss b/packages/dataviews/src/dataviews-layouts/list/style.scss index 8fe048cab77b51..65c28a90608a6b 100644 --- a/packages/dataviews/src/dataviews-layouts/list/style.scss +++ b/packages/dataviews/src/dataviews-layouts/list/style.scss @@ -50,7 +50,7 @@ ul.dataviews-view-list { } &:not(.is-selected) { - .dataviews-view-list__primary-field { + .dataviews-view-list__title-field { color: $gray-900; } &:hover, @@ -59,7 +59,7 @@ ul.dataviews-view-list { color: var(--wp-admin-theme-color); background-color: #f8f8f8; - .dataviews-view-list__primary-field, + .dataviews-view-list__title-field, .dataviews-view-list__fields { color: var(--wp-admin-theme-color); } @@ -74,7 +74,7 @@ ul.dataviews-view-list { background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04); color: $gray-900; - .dataviews-view-list__primary-field, + .dataviews-view-list__title-field, .dataviews-view-list__fields { color: var(--wp-admin-theme-color); } @@ -106,7 +106,7 @@ ul.dataviews-view-list { } } } - .dataviews-view-list__primary-field { + .dataviews-view-list__title-field { flex: 1; min-height: $grid-unit-30; line-height: $grid-unit-30; From 7159a7857fb9106c4ecaa299220de91f0fff23c8 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 10 Dec 2024 16:01:26 +1100 Subject: [PATCH 19/50] Add stylebook screen for classic themes (#66851) Unlinked contributors: acketon. Co-authored-by: tellthemachines Co-authored-by: youknowriad Co-authored-by: ramonjd Co-authored-by: t-hamano Co-authored-by: jasmussen Co-authored-by: annezazu Co-authored-by: mtias Co-authored-by: carlomanf Co-authored-by: daveloodts Co-authored-by: cbirdsong Co-authored-by: fabiankaegy Co-authored-by: mrwweb Co-authored-by: ltrihan Co-authored-by: masteradhoc --- backport-changelog/6.8/7865.md | 3 + lib/block-editor-settings.php | 13 ++ lib/compat/wordpress-6.8/site-editor.php | 26 ++- .../src/components/maybe-editor/index.js | 58 +++++++ .../src/components/resizable-frame/index.js | 11 +- .../sidebar-navigation-screen-main/index.js | 105 +++++++----- .../index.js | 7 - .../src/components/site-editor-routes/home.js | 4 +- .../components/site-editor-routes/index.js | 2 + .../site-editor-routes/stylebook.js | 28 ++++ .../site-editor-routes/template-item.js | 6 +- .../site-editor-routes/template-part-item.js | 6 +- .../src/components/style-book/examples.tsx | 4 +- .../src/components/style-book/index.js | 155 ++++++++++++++---- .../src/components/style-book/style.scss | 4 + test/e2e/specs/site-editor/navigation.spec.js | 16 +- test/e2e/specs/site-editor/style-book.spec.js | 24 +++ 17 files changed, 375 insertions(+), 97 deletions(-) create mode 100644 backport-changelog/6.8/7865.md create mode 100644 packages/edit-site/src/components/maybe-editor/index.js create mode 100644 packages/edit-site/src/components/site-editor-routes/stylebook.js diff --git a/backport-changelog/6.8/7865.md b/backport-changelog/6.8/7865.md new file mode 100644 index 00000000000000..f7b23c944dc327 --- /dev/null +++ b/backport-changelog/6.8/7865.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7865 + +* https://github.com/WordPress/gutenberg/pull/66851 \ No newline at end of file diff --git a/lib/block-editor-settings.php b/lib/block-editor-settings.php index defd7cd391b16b..6448eb2e524853 100644 --- a/lib/block-editor-settings.php +++ b/lib/block-editor-settings.php @@ -53,6 +53,13 @@ function gutenberg_get_block_editor_settings( $settings ) { $global_styles[] = $block_classes; } + // Get any additional css from the customizer and add it before global styles custom CSS. + $global_styles[] = array( + 'css' => wp_get_custom_css(), + '__unstableType' => 'user', + 'isGlobalStyles' => false, + ); + /* * Add the custom CSS as a separate stylesheet so any invalid CSS * entered by users does not break other global styles. @@ -74,6 +81,12 @@ function gutenberg_get_block_editor_settings( $settings ) { $block_classes['css'] = $actual_css; $global_styles[] = $block_classes; } + // Get any additional css from the customizer. + $global_styles[] = array( + 'css' => wp_get_custom_css(), + '__unstableType' => 'user', + 'isGlobalStyles' => false, + ); } $settings['styles'] = array_merge( $global_styles, get_block_editor_theme_styles() ); diff --git a/lib/compat/wordpress-6.8/site-editor.php b/lib/compat/wordpress-6.8/site-editor.php index cde108830b1d2c..53d04c2e543f48 100644 --- a/lib/compat/wordpress-6.8/site-editor.php +++ b/lib/compat/wordpress-6.8/site-editor.php @@ -116,9 +116,33 @@ function gutenberg_redirect_site_editor_deprecated_urls() { * @return callable The default handler or a custom handler. */ function gutenberg_styles_wp_die_handler( $default_handler ) { - if ( ! wp_is_block_theme() && str_contains( $_SERVER['REQUEST_URI'], 'site-editor.php' ) && isset( $_GET['p'] ) ) { + if ( ! wp_is_block_theme() && str_contains( $_SERVER['REQUEST_URI'], 'site-editor.php' ) && current_user_can( 'edit_theme_options' ) ) { return '__return_false'; } return $default_handler; } add_filter( 'wp_die_handler', 'gutenberg_styles_wp_die_handler' ); + +/** + * Add a Styles submenu under the Appearance menu + * for Classic themes. + * + * @global array $submenu + */ +function gutenberg_add_styles_submenu_item() { + if ( ! wp_is_block_theme() && ( current_theme_supports( 'editor-styles' ) || wp_theme_has_theme_json() ) ) { + global $submenu; + + $styles_menu_item = array( + __( 'Design', 'gutenberg' ), + 'edit_theme_options', + 'site-editor.php', + ); + // If $submenu exists, insert the Styles submenu item at position 2. + if ( $submenu && isset( $submenu['themes.php'] ) ) { + // This might not work as expected if the submenu has already been modified. + array_splice( $submenu['themes.php'], 1, 1, array( $styles_menu_item ) ); + } + } +} +add_action( 'admin_init', 'gutenberg_add_styles_submenu_item' ); diff --git a/packages/edit-site/src/components/maybe-editor/index.js b/packages/edit-site/src/components/maybe-editor/index.js new file mode 100644 index 00000000000000..bee1c427c87b47 --- /dev/null +++ b/packages/edit-site/src/components/maybe-editor/index.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ + +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ + +import Editor from '../editor'; + +export function MaybeEditor( { showEditor = true } ) { + const { isBlockBasedTheme, siteUrl } = useSelect( ( select ) => { + const { getEntityRecord, getCurrentTheme } = select( coreStore ); + const siteData = getEntityRecord( 'root', '__unstableBase' ); + + return { + isBlockBasedTheme: getCurrentTheme()?.is_block_theme, + siteUrl: siteData?.home, + }; + }, [] ); + + // If theme is block based, return the Editor, otherwise return the site preview. + return isBlockBasedTheme || showEditor ? ( + + ) : ( +