diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index 4c82dc6683f370..398b53b340673b 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -1,6 +1,6 @@ { const { getSettings, + getBlockInsertionPoint, getBlockOrder, getSelectionStart, getSelectedBlockClientId, getHoveredBlockClientId, + isBlockInsertionPointVisible, } = select( blockEditorStore ); const { sectionRootClientId: root } = unlock( getSettings() ); - // To do: move ZoomOutModeInserters to core/editor. - // Or we perhaps we should move the insertion point state to the - // block-editor store. I'm not sure what it was ever moved to the editor - // store, because all the inserter components all live in the - // block-editor package. - // eslint-disable-next-line @wordpress/data-no-store-string-literals - const editor = select( 'core/editor' ); return { hasSelection: !! getSelectionStart().clientId, + blockInsertionPoint: getBlockInsertionPoint(), blockOrder: getBlockOrder( root ), - insertionPoint: unlock( editor ).getInsertionPoint(), + blockInsertionPointVisible: isBlockInsertionPointVisible(), sectionRootClientId: root, setInserterIsOpened: getSettings().__experimentalSetIsInserterOpened, @@ -50,6 +47,8 @@ function ZoomOutModeInserters() { }; }, [] ); + const { showInsertionPoint } = useDispatch( blockEditorStore ); + // Defer the initial rendering to avoid the jumps due to the animation. useEffect( () => { const timeout = setTimeout( () => { @@ -65,14 +64,8 @@ function ZoomOutModeInserters() { } return [ undefined, ...blockOrder ].map( ( clientId, index ) => { - const shouldRenderInserter = insertionPoint.insertionIndex !== index; - const shouldRenderInsertionPoint = - insertionPoint.insertionIndex === index; - - if ( ! shouldRenderInserter && ! shouldRenderInsertionPoint ) { - return null; - } + blockInsertionPointVisible && blockInsertionPoint.index === index; const previousClientId = clientId; const nextClientId = blockOrder[ index ]; @@ -104,7 +97,7 @@ function ZoomOutModeInserters() { className="block-editor-block-list__insertion-point-indicator" /> ) } - { shouldRenderInserter && ( + { ! shouldRenderInsertionPoint && ( { @@ -114,6 +107,9 @@ function ZoomOutModeInserters() { tab: 'patterns', category: 'all', } ); + showInsertionPoint( sectionRootClientId, index, { + operation: 'insert', + } ); } } /> ) } diff --git a/packages/block-editor/src/components/child-layout-control/index.js b/packages/block-editor/src/components/child-layout-control/index.js index 698ea2d2d74a42..022acf2e1074a4 100644 --- a/packages/block-editor/src/components/child-layout-control/index.js +++ b/packages/block-editor/src/components/child-layout-control/index.js @@ -174,6 +174,8 @@ function FlexControls( { } ); } } value={ flexSize } + label={ flexResetLabel } + hideLabelFromVision /> ) } diff --git a/packages/block-editor/src/components/grid/grid-item-resizer.js b/packages/block-editor/src/components/grid/grid-item-resizer.js index 6f6fa655b35568..34bc1db6048067 100644 --- a/packages/block-editor/src/components/grid/grid-item-resizer.js +++ b/packages/block-editor/src/components/grid/grid-item-resizer.js @@ -98,7 +98,7 @@ function GridItemResizerInner( { - ); -} diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 17a98570843014..e0bc29d62e1b9a 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -26,7 +26,7 @@ import Tips from './tips'; import InserterPreviewPanel from './preview-panel'; import BlockTypesTab from './block-types-tab'; import BlockPatternsTab from './block-patterns-tab'; -import { PatternCategoryPreviewPanel } from './block-patterns-tab/pattern-category-preview-panel'; +import { PatternCategoryPreviews } from './block-patterns-tab/pattern-category-previews'; import { MediaTab, MediaCategoryPanel } from './media-tab'; import InserterSearchResults from './search-results'; import useInsertionPoint from './hooks/use-insertion-point'; @@ -246,7 +246,7 @@ function InserterMenu( selectedCategory={ selectedPatternCategory } > { showPatternPanel && ( - ) } diff --git a/packages/block-editor/src/components/use-settings/index.js b/packages/block-editor/src/components/use-settings/index.js index b0410b404d5e98..e356260c2d6b38 100644 --- a/packages/block-editor/src/components/use-settings/index.js +++ b/packages/block-editor/src/components/use-settings/index.js @@ -43,9 +43,10 @@ export function useSettings( ...paths ) { * It looks up the setting first in the block instance hierarchy. * If none is found, it'll look it up in the block editor settings. * + * @deprecated 6.5.0 Use useSettings instead. + * * @param {string} path The path to the setting. * @return {any} Returns the value defined for the setting. - * @deprecated 6.5.0 Use useSettings instead. * @example * ```js * const isEnabled = useSetting( 'typography.dropCap' ); diff --git a/packages/block-library/src/categories/block.json b/packages/block-library/src/categories/block.json index 7f74befa3b6816..f192087dfb4617 100644 --- a/packages/block-library/src/categories/block.json +++ b/packages/block-library/src/categories/block.json @@ -62,6 +62,18 @@ }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } } }, "editorStyle": "wp-block-categories-editor", diff --git a/packages/block-library/src/column/block.json b/packages/block-library/src/column/block.json index 0857abb47ffdc0..33bd528b356880 100644 --- a/packages/block-library/src/column/block.json +++ b/packages/block-library/src/column/block.json @@ -48,10 +48,12 @@ }, "__experimentalBorder": { "color": true, + "radius": true, "style": true, "width": true, "__experimentalDefaultControls": { "color": true, + "radius": true, "style": true, "width": true } diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index c43137c632b73f..52f3aa64287fae 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -44,6 +44,7 @@ @import "./social-links/editor.scss"; @import "./spacer/editor.scss"; @import "./table/editor.scss"; +@import "./tag-cloud/editor.scss"; @import "./template-part/editor.scss"; @import "./text-columns/editor.scss"; @import "./video/editor.scss"; diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 7010e9b7b2219e..0bc0dfaacea1a2 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -188,7 +188,7 @@ const { state, actions, callbacks } = store( }, callbacks: { setOverlayStyles() { - if ( ! state.currentImage.imageRef ) { + if ( ! state.overlayEnabled ) { return; } diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index cd6b7a262c0081..81c21a236322ce 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -6,7 +6,6 @@ import { TextControl, SelectControl, RangeControl, - ToggleControl, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, Notice, @@ -103,9 +102,9 @@ export default function QueryInspectorControls( props ) { const showInheritControl = isControlAllowed( allowedControls, 'inherit' ); const showPostTypeControl = ! inherit && isControlAllowed( allowedControls, 'postType' ); - const postTypeControlLabel = __( 'Content type' ); + const postTypeControlLabel = __( 'Post type' ); const postTypeControlHelp = __( - 'WordPress contains different types of content you can filter by. Posts and pages are the default types, but plugins could add more.' + 'Select the type of content to display: posts, pages, or custom post types.' ); const showColumnsControl = false; const showOrderControl = @@ -146,17 +145,33 @@ export default function QueryInspectorControls( props ) { { showSettingsPanel && ( { showInheritControl && ( - - setQuery( { inherit: !! value } ) + { + setQuery( { inherit: !! value } ); + } } + help={ + inherit + ? __( + 'Display a list of posts or custom post types based on the current template.' + ) + : __( + 'Display a list of posts or custom post types based on specific criteria.' + ) } - /> + value={ !! inherit } + > + + + ) } { showPostTypeControl && ( postTypesSelectOptions.length > 2 ? ( diff --git a/packages/block-library/src/spacer/controls.js b/packages/block-library/src/spacer/controls.js index 160335fcc092e9..1e899e15aff0de 100644 --- a/packages/block-library/src/spacer/controls.js +++ b/packages/block-library/src/spacer/controls.js @@ -10,7 +10,6 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { - BaseControl, PanelBody, __experimentalUseCustomUnits as useCustomUnits, __experimentalUnitControl as UnitControl, @@ -57,19 +56,17 @@ function DimensionInput( { label, onChange, isResizing, value = '' } ) { return ( <> { ( ! spacingSizes || spacingSizes?.length === 0 ) && ( - - - + ) } - { spacingSizes?.length > 0 && ( 0 ? mergedArrays : undefined; + return mergedArrays.length > 0 ? mergedArrays : undefined; +} +export function blockBindingsSources( state = {}, action ) { switch ( action.type ) { case 'ADD_BLOCK_BINDINGS_SOURCE': return { @@ -388,7 +393,10 @@ export function blockBindingsSources( state = {}, action ) { [ action.name ]: { // Don't override the label if it's already set. label: state[ action.name ]?.label || action.label, - usesContext: mergedUsesContext, + usesContext: getMergedUsesContext( + state[ action.name ]?.usesContext, + action.usesContext + ), getValues: action.getValues, setValues: action.setValues, getPlaceholder: action.getPlaceholder, @@ -405,7 +413,10 @@ export function blockBindingsSources( state = {}, action ) { */ ...state[ action.name ], label: action.label, - usesContext: mergedUsesContext, + usesContext: getMergedUsesContext( + state[ action.name ]?.usesContext, + action.usesContext + ), }, }; case 'REMOVE_BLOCK_BINDINGS_SOURCE': diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4a423eeb4a57df..f8666e96a20a15 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- `CustomSelectControl`: Restore `describedBy` functionality ([#63957](https://github.com/WordPress/gutenberg/pull/63957)). + ### Internal - `DropdownMenuV2`: break menu item help text on multiple lines for better truncation. ([#63916](https://github.com/WordPress/gutenberg/pull/63916)). @@ -17,6 +21,7 @@ - `ComboboxControl`: Fix ComboboxControl reset button when using the keyboard. ([#63410](https://github.com/WordPress/gutenberg/pull/63410)) - `Button`: Never apply `aria-disabled` to anchor ([#63376](https://github.com/WordPress/gutenberg/pull/63376)). - `SelectControl`: Fix hover/focus color in wp-admin ([#63855](https://github.com/WordPress/gutenberg/pull/63855)). +- `ToggleControl`: Fix indentation ([#63903](https://github.com/WordPress/gutenberg/pull/63903)). ### Enhancements diff --git a/packages/components/src/custom-select-control/index.tsx b/packages/components/src/custom-select-control/index.tsx index 5bce6afc4d92b6..8735b8d320991e 100644 --- a/packages/components/src/custom-select-control/index.tsx +++ b/packages/components/src/custom-select-control/index.tsx @@ -4,6 +4,12 @@ import * as Ariakit from '@ariakit/react'; import clsx from 'clsx'; +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { __, sprintf } from '@wordpress/i18n'; + /** * Internal dependencies */ @@ -11,6 +17,7 @@ import _CustomSelect from '../custom-select-control-v2/custom-select'; import CustomSelectItem from '../custom-select-control-v2/item'; import * as Styled from '../custom-select-control-v2/styles'; import type { CustomSelectProps } from './types'; +import { VisuallyHidden } from '../visually-hidden'; function useDeprecatedProps( { __experimentalShowSelectedHint, @@ -35,6 +42,15 @@ function applyOptionDeprecations( { }; } +function getDescribedBy( currentValue: string, describedBy?: string ) { + if ( describedBy ) { + return describedBy; + } + + // translators: %s: The selected option. + return sprintf( __( 'Currently selected: %s' ), currentValue ); +} + function CustomSelectControl( props: CustomSelectProps ) { const { __next40pxDefaultSize = false, @@ -48,8 +64,13 @@ function CustomSelectControl( props: CustomSelectProps ) { ...restProps } = useDeprecatedProps( props ); + const descriptionId = useInstanceId( + CustomSelectControl, + 'custom-select-control__description' + ); + // Forward props + store from v2 implementation - const store = Ariakit.useSelectStore( { + const store = Ariakit.useSelectStore< string >( { async setValue( nextValue ) { const nextOption = options.find( ( item ) => item.name === nextValue @@ -117,9 +138,9 @@ function CustomSelectControl( props: CustomSelectProps ) { ); } ); - const renderSelectedValueHint = () => { - const { value: currentValue } = store.getState(); + const { value: currentValue } = store.getState(); + const renderSelectedValueHint = () => { const selectedOptionHint = options ?.map( applyOptionDeprecations ) ?.find( ( { name } ) => currentValue === name )?.hint; @@ -153,23 +174,30 @@ function CustomSelectControl( props: CustomSelectProps ) { } )(); return ( - <_CustomSelect - aria-describedby={ describedBy } - renderSelectedValue={ - showSelectedHint ? renderSelectedValueHint : undefined - } - size={ translatedSize } - store={ store } - className={ clsx( - // Keeping the classname for legacy reasons - 'components-custom-select-control', - classNameProp - ) } - isLegacy - { ...restProps } - > - { children } - + <> + <_CustomSelect + aria-describedby={ descriptionId } + renderSelectedValue={ + showSelectedHint ? renderSelectedValueHint : undefined + } + size={ translatedSize } + store={ store } + className={ clsx( + // Keeping the classname for legacy reasons + 'components-custom-select-control', + classNameProp + ) } + isLegacy + { ...restProps } + > + { children } + + + + { getDescribedBy( currentValue, describedBy ) } + + + ); } diff --git a/packages/components/src/custom-select-control/test/index.tsx b/packages/components/src/custom-select-control/test/index.tsx index 73dcb039df3f1b..4bc96f318138c5 100644 --- a/packages/components/src/custom-select-control/test/index.tsx +++ b/packages/components/src/custom-select-control/test/index.tsx @@ -670,5 +670,27 @@ describe.each( [ expect( currentSelectedItem ).not.toHaveFocus(); expect( onBlurMock ).toHaveBeenCalledTimes( 1 ); } ); + + it( 'should render the describedBy text when specified', async () => { + const describedByText = 'My description.'; + + render( + + ); + + expect( + screen.getByRole( 'combobox' ) + ).toHaveAccessibleDescription( describedByText ); + } ); + + it( 'should render the default ARIA description when describedBy is not specified', async () => { + render( ); + + expect( + screen.getByRole( 'combobox' ) + ).toHaveAccessibleDescription( + `Currently selected: ${ props.options[ 0 ].name }` + ); + } ); } ); } ); diff --git a/packages/components/src/toggle-control/style.scss b/packages/components/src/toggle-control/style.scss index 263a372b00fe84..68733b53a9bc94 100644 --- a/packages/components/src/toggle-control/style.scss +++ b/packages/components/src/toggle-control/style.scss @@ -7,5 +7,6 @@ } .components-toggle-control__help { - margin-left: $toggle-width + $grid-unit-10; + display: inline-block; + margin-inline-start: $toggle-width + $grid-unit-10; } diff --git a/packages/core-data/src/footnotes/get-rich-text-values-cached.js b/packages/core-data/src/footnotes/get-rich-text-values-cached.js index 06a01c5ef63fdd..a5c2d258108612 100644 --- a/packages/core-data/src/footnotes/get-rich-text-values-cached.js +++ b/packages/core-data/src/footnotes/get-rich-text-values-cached.js @@ -6,7 +6,7 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { unlock } from '../private-apis'; +import { unlock } from '../lock-unlock'; // TODO: The following line should have been: // diff --git a/packages/core-data/src/hooks/use-entity-records.ts b/packages/core-data/src/hooks/use-entity-records.ts index 5d643ab8896925..e2659b88bc0198 100644 --- a/packages/core-data/src/hooks/use-entity-records.ts +++ b/packages/core-data/src/hooks/use-entity-records.ts @@ -4,6 +4,7 @@ import { addQueryArgs } from '@wordpress/url'; import deprecated from '@wordpress/deprecated'; import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -12,6 +13,7 @@ import useQuerySelect from './use-query-select'; import { store as coreStore } from '../'; import type { Options } from './use-entity-record'; import type { Status } from './constants'; +import { unlock } from '../lock-unlock'; interface EntityRecordsResolution< RecordType > { /** The requested entity record */ @@ -152,3 +154,51 @@ export function __experimentalUseEntityRecords( } ); return useEntityRecords( kind, name, queryArgs, options ); } + +export function useEntityRecordsWithPermissions< RecordType >( + kind: string, + name: string, + queryArgs: Record< string, unknown > = {}, + options: Options = { enabled: true } +): EntityRecordsResolution< RecordType > { + const entityConfig = useSelect( + ( select ) => select( coreStore ).getEntityConfig( kind, name ), + [ kind, name ] + ); + const { records: data, ...ret } = useEntityRecords( + kind, + name, + queryArgs, + options + ); + const ids = useMemo( + () => + data?.map( + // @ts-ignore + ( record: RecordType ) => record[ entityConfig?.key ?? 'id' ] + ) ?? [], + [ data, entityConfig?.key ] + ); + + const permissions = useSelect( + ( select ) => { + const { getEntityRecordsPermissions } = unlock( + select( coreStore ) + ); + return getEntityRecordsPermissions( kind, name, ids ); + }, + [ ids, kind, name ] + ); + + const dataWithPermissions = useMemo( + () => + data?.map( ( record, index ) => ( { + // @ts-ignore + ...record, + permissions: permissions[ index ], + } ) ) ?? [], + [ data, permissions ] + ); + + return { records: dataWithPermissions, ...ret }; +} diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index bd25fa8de9902b..ad6adec0203c59 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -18,7 +18,7 @@ import { getMethodName, } from './entities'; import { STORE_NAME } from './name'; -import { unlock } from './private-apis'; +import { unlock } from './lock-unlock'; // The entity selectors/resolvers and actions are shortcuts to their generic equivalents // (getEntityRecord, getEntityRecords, updateEntityRecord, updateEntityRecords) @@ -86,3 +86,4 @@ export * from './entity-provider'; export * from './entity-types'; export * from './fetch'; export * from './hooks'; +export * from './private-apis'; diff --git a/packages/core-data/src/lock-unlock.js b/packages/core-data/src/lock-unlock.js new file mode 100644 index 00000000000000..91bf30792c970a --- /dev/null +++ b/packages/core-data/src/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/core-data' + ); diff --git a/packages/core-data/src/private-apis.js b/packages/core-data/src/private-apis.js index 91bf30792c970a..443db97957285d 100644 --- a/packages/core-data/src/private-apis.js +++ b/packages/core-data/src/private-apis.js @@ -1,10 +1,10 @@ /** - * WordPress dependencies + * Internal dependencies */ -import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; +import { useEntityRecordsWithPermissions } from './hooks/use-entity-records'; +import { lock } from './lock-unlock'; -export const { lock, unlock } = - __dangerousOptInToUnstableAPIsOnlyForCoreModules( - 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', - '@wordpress/core-data' - ); +export const privateApis = {}; +lock( privateApis, { + useEntityRecordsWithPermissions, +} ); diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 6280bb96319634..9a9b2ef5100784 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -50,3 +50,26 @@ export const getBlockPatternsForPostType = createRegistrySelector( () => [ select( STORE_NAME ).getBlockPatterns() ] ) ); + +/** + * Returns the entity records permissions for the given entity record ids. + */ +export const getEntityRecordsPermissions = createRegistrySelector( ( select ) => + createSelector( + ( state: State, kind: string, name: string, ids: string[] ) => { + return ids.map( ( id ) => ( { + delete: select( STORE_NAME ).canUser( 'delete', { + kind, + name, + id, + } ), + update: select( STORE_NAME ).canUser( 'update', { + kind, + name, + id, + } ), + } ) ); + }, + ( state ) => [ state.userPermissions ] + ) +); diff --git a/packages/dataviews/src/components/dataform/index.tsx b/packages/dataviews/src/components/dataform/index.tsx index 64196c685a9784..42a6766813975e 100644 --- a/packages/dataviews/src/components/dataform/index.tsx +++ b/packages/dataviews/src/components/dataform/index.tsx @@ -6,13 +6,16 @@ import type { Dispatch, SetStateAction } from 'react'; /** * WordPress dependencies */ -import { TextControl } from '@wordpress/components'; +import { + TextControl, + __experimentalNumberControl as NumberControl, +} from '@wordpress/components'; import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import type { Form, Field, NormalizedField } from '../../types'; +import type { Form, Field, NormalizedField, FieldType } from '../../types'; import { normalizeFields } from '../../normalize-fields'; type DataFormProps< Item > = { @@ -56,12 +59,41 @@ function DataFormTextControl< Item >( { ); } +function DataFormNumberControl< Item >( { + data, + field, + onChange, +}: DataFormControlProps< Item > ) { + const { id, label, description } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string | undefined ) => + onChange( ( prevItem: Item ) => ( { + ...prevItem, + [ id ]: newValue, + } ) ), + [ id, onChange ] + ); + + return ( + + ); +} + const controls: { - [ key: string ]: < Item >( + [ key in FieldType ]: < Item >( props: DataFormControlProps< Item > ) => JSX.Element; } = { text: DataFormTextControl, + integer: DataFormNumberControl, }; function getControlForField< Item >( field: NormalizedField< Item > ) { diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 2e288c8e11d41c..a67eaa6b76f042 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -20,15 +20,21 @@ const fields = [ label: 'Title', type: 'text' as const, }, + { + id: 'order', + label: 'Order', + type: 'integer' as const, + }, ]; export const Default = () => { const [ post, setPost ] = useState( { title: 'Hello, World!', + order: 2, } ); const form = { - visibleFields: [ 'title' ], + visibleFields: [ 'title', 'order' ], }; return ( diff --git a/packages/dataviews/src/index.ts b/packages/dataviews/src/index.ts index 8b6e53e1ff7293..95a8ab4c2e5e8d 100644 --- a/packages/dataviews/src/index.ts +++ b/packages/dataviews/src/index.ts @@ -3,3 +3,4 @@ export { default as DataForm } from './components/dataform'; export { VIEW_LAYOUTS } from './layouts'; export { filterSortAndPaginate } from './filter-and-sort-data-view'; export type * from './types'; +export { isItemValid } from './validation'; diff --git a/packages/dataviews/src/layouts/grid/style.scss b/packages/dataviews/src/layouts/grid/style.scss index 91f0e2b8a381c9..6fe99642784dc9 100644 --- a/packages/dataviews/src/layouts/grid/style.scss +++ b/packages/dataviews/src/layouts/grid/style.scss @@ -61,6 +61,12 @@ padding: 0 0 $grid-unit-15; } + .dataviews-view-grid__field-value:not(:empty) { + min-height: $grid-unit-30; + line-height: $grid-unit-05 * 5; + padding-top: math.div($grid-unit-05, 2); + } + .dataviews-view-grid__field { align-items: flex-start; min-height: $grid-unit-30; diff --git a/packages/dataviews/src/layouts/list/style.scss b/packages/dataviews/src/layouts/list/style.scss index 09f29a73b74d2d..5b8f764d012a22 100644 --- a/packages/dataviews/src/layouts/list/style.scss +++ b/packages/dataviews/src/layouts/list/style.scss @@ -179,7 +179,10 @@ } .dataviews-view-list__field-value { - line-height: $grid-unit-05 * 6; + min-height: $grid-unit-30; + line-height: $grid-unit-05 * 5; + display: flex; + align-items: center; } } diff --git a/packages/dataviews/src/test/validation.ts b/packages/dataviews/src/test/validation.ts new file mode 100644 index 00000000000000..d90d4744ac3272 --- /dev/null +++ b/packages/dataviews/src/test/validation.ts @@ -0,0 +1,63 @@ +/** + * Internal dependencies + */ +import { isItemValid } from '../validation'; +import type { Field } from '../types'; + +describe( 'validation', () => { + it( 'fields not visible in form are not validated', () => { + const item = { id: 1, valid_order: 2, invalid_order: 'd' }; + const fields: Field< {} >[] = [ + { + id: 'valid_order', + type: 'integer', + }, + { + id: 'invalid_order', + type: 'integer', + }, + ]; + const form = { visibleFields: [ 'valid_order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'integer field is valid if value is integer', () => { + const item = { id: 1, order: 2, title: 'hi' }; + const fields: Field< {} >[] = [ + { + type: 'integer', + id: 'order', + }, + ]; + const form = { visibleFields: [ 'order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'integer field is invalid if value is not integer', () => { + const item = { id: 1, order: 'd' }; + const fields: Field< {} >[] = [ + { + id: 'order', + type: 'integer', + }, + ]; + const form = { visibleFields: [ 'order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'integer field is invalid if value is empty', () => { + const item = { id: 1, order: '' }; + const fields: Field< {} >[] = [ + { + id: 'order', + type: 'integer', + }, + ]; + const form = { visibleFields: [ 'order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); +} ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 0b43740efc3f2d..37c3efbde5cfb0 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -44,7 +44,7 @@ export type Operator = export type ItemRecord = Record< string, unknown >; -export type FieldType = 'text'; +export type FieldType = 'text' | 'integer'; /** * A dataview field for a specific property of a data type. @@ -65,6 +65,11 @@ export type Field< Item > = { */ label?: string; + /** + * A description of the field. + */ + description?: string; + /** * Placeholder for the field. */ diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts new file mode 100644 index 00000000000000..5b20d094a41861 --- /dev/null +++ b/packages/dataviews/src/validation.ts @@ -0,0 +1,33 @@ +/** + * Internal dependencies + */ +import { normalizeFields } from './normalize-fields'; +import type { Field, Form } from './types'; + +export function isItemValid< Item >( + item: Item, + fields: Field< Item >[], + form: Form +): boolean { + const _fields = normalizeFields( + fields.filter( ( { id } ) => !! form.visibleFields?.includes( id ) ) + ); + return _fields.every( ( field ) => { + const value = field.getValue( { item } ); + + // TODO: this implicitely means the value is required. + if ( field.type === 'integer' && value === '' ) { + return false; + } + + if ( + field.type === 'integer' && + ! Number.isInteger( Number( value ) ) + ) { + return false; + } + + // Nothing to validate. + return true; + } ); +} diff --git a/packages/edit-site/src/components/global-styles/font-families.js b/packages/edit-site/src/components/global-styles/font-families.js index 7a05a66c6efdfe..f6610d0457bbae 100644 --- a/packages/edit-site/src/components/global-styles/font-families.js +++ b/packages/edit-site/src/components/global-styles/font-families.js @@ -69,7 +69,7 @@ function FontFamilies() { _x( 'Theme', 'font source' ) } - + { themeFonts.map( ( font ) => ( - + { customFonts.map( ( font ) => ( + select( editorStore ).getEditorSettings().fontLibraryEnabled, + [] + ); + + return ( + <> + +
+ + + + { fontLibraryEnabled && } + +
+ + ); +} + +export default ScreenTypeset; diff --git a/packages/edit-site/src/components/global-styles/screen-typography.js b/packages/edit-site/src/components/global-styles/screen-typography.js index a58802a204ce32..c23592c51a6a2a 100644 --- a/packages/edit-site/src/components/global-styles/screen-typography.js +++ b/packages/edit-site/src/components/global-styles/screen-typography.js @@ -3,17 +3,17 @@ */ import { __ } from '@wordpress/i18n'; import { __experimentalVStack as VStack } from '@wordpress/components'; -import { store as editorStore } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ import TypographyElements from './typography-elements'; -import TypographyVariations from './variations/variations-typography'; -import FontFamilies from './font-families'; import ScreenHeader from './header'; import FontSizesCount from './font-sizes/font-sizes-count'; +import TypesetButton from './typeset-button'; +import FontFamilies from './font-families'; function ScreenTypography() { const fontLibraryEnabled = useSelect( @@ -32,9 +32,9 @@ function ScreenTypography() { />
+ { fontLibraryEnabled && } -
diff --git a/packages/edit-site/src/components/global-styles/typeset-button.js b/packages/edit-site/src/components/global-styles/typeset-button.js new file mode 100644 index 00000000000000..d66310f2ed8ff5 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/typeset-button.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { isRTL, __ } from '@wordpress/i18n'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalVStack as VStack, + __experimentalHStack as HStack, + FlexItem, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; +import { useMemo, useContext } from '@wordpress/element'; +import { Icon, chevronLeft, chevronRight } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import FontLibraryProvider from './font-library-modal/context'; +import { getFontFamilies } from './utils'; +import { NavigationButtonAsItem } from './navigation-button'; +import Subtitle from './subtitle'; +import { unlock } from '../../lock-unlock'; +import { filterObjectByProperties } from '../../hooks/use-theme-style-variations/use-theme-style-variations-by-property'; + +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); + +function TypesetButton() { + const { base } = useContext( GlobalStylesContext ); + const { user: userConfig } = useContext( GlobalStylesContext ); + const config = mergeBaseAndUserConfigs( base, userConfig ); + const allFontFamilies = getFontFamilies( config ); + const hasFonts = + allFontFamilies.filter( ( font ) => font !== null ).length > 0; + const variations = useSelect( ( select ) => { + return select( + coreStore + ).__experimentalGetCurrentThemeGlobalStylesVariations(); + }, [] ); + const userTypographyConfig = filterObjectByProperties( + userConfig, + 'typography' + ); + + const title = useMemo( () => { + if ( Object.keys( userTypographyConfig ).length === 0 ) { + return __( 'Default' ); + } + const activeVariation = variations.find( ( variation ) => { + return ( + JSON.stringify( + filterObjectByProperties( variation, 'typography' ) + ) === JSON.stringify( userTypographyConfig ) + ); + } ); + if ( activeVariation ) { + return activeVariation.title; + } + return allFontFamilies.map( ( font ) => font?.name ).join( ', ' ); + }, [ userTypographyConfig, variations ] ); + + return ( + hasFonts && ( + + + { __( 'Typeset' ) } + + + + + { title } + + + + + + ) + ); +} + +export default ( { ...props } ) => ( + + + +); diff --git a/packages/edit-site/src/components/global-styles/typeset.js b/packages/edit-site/src/components/global-styles/typeset.js new file mode 100644 index 00000000000000..e99e6a037500ad --- /dev/null +++ b/packages/edit-site/src/components/global-styles/typeset.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalVStack as VStack, + __experimentalHStack as HStack, +} from '@wordpress/components'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import FontLibraryProvider, { + FontLibraryContext, +} from './font-library-modal/context'; +import FontLibraryModal from './font-library-modal'; +import FontFamilyItem from './font-family-item'; +import Subtitle from './subtitle'; +import { getFontFamilies } from './utils'; +import { unlock } from '../../lock-unlock'; + +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); + +function Typesets() { + const { modalTabOpen, setModalTabOpen } = useContext( FontLibraryContext ); + const { base } = useContext( GlobalStylesContext ); + const { user: userConfig } = useContext( GlobalStylesContext ); + const config = mergeBaseAndUserConfigs( base, userConfig ); + const allFontFamilies = getFontFamilies( config ); + const hasFonts = + allFontFamilies.filter( ( font ) => font !== null ).length > 0; + + return ( + hasFonts && ( + <> + { !! modalTabOpen && ( + setModalTabOpen( null ) } + defaultTabId={ modalTabOpen } + /> + ) } + + + + { __( 'Fonts' ) } + + + { allFontFamilies.map( + ( font ) => + font && ( + + ) + ) } + + + + ) + ); +} + +export default ( { ...props } ) => ( + + + +); diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 40d20bc1ec86f8..54bd4f97390a8f 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -32,6 +32,7 @@ import { } from './screen-block-list'; import ScreenBlock from './screen-block'; import ScreenTypography from './screen-typography'; +import ScreenTypeset from './screen-typeset'; import ScreenTypographyElement from './screen-typography-element'; import FontSize from './font-sizes/font-size'; import FontSizes from './font-sizes/font-sizes'; @@ -323,6 +324,10 @@ function GlobalStylesUI() { + + + + diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js index 6096b381fb2187..66a25854a06fe1 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -52,7 +52,19 @@ function getFontFamilyFromSetting( fontFamilies, setting ) { } export function getFontFamilies( themeJson ) { - const fontFamilies = themeJson?.settings?.typography?.fontFamilies?.theme; // TODO this could not be under theme. + const themeFontFamilies = + themeJson?.settings?.typography?.fontFamilies?.theme; + const customFontFamilies = + themeJson?.settings?.typography?.fontFamilies?.custom; + + let fontFamilies = []; + if ( themeFontFamilies && customFontFamilies ) { + fontFamilies = [ ...themeFontFamilies, ...customFontFamilies ]; + } else if ( themeFontFamilies ) { + fontFamilies = themeFontFamilies; + } else if ( customFontFamilies ) { + fontFamilies = customFontFamilies; + } const bodyFontFamilySetting = themeJson?.styles?.typography?.fontFamily; const bodyFontFamily = getFontFamilyFromSetting( fontFamilies, diff --git a/packages/edit-site/src/components/page-patterns/fields.js b/packages/edit-site/src/components/page-patterns/fields.js new file mode 100644 index 00000000000000..eab6dbca32833a --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/fields.js @@ -0,0 +1,251 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { + __experimentalHStack as HStack, + Button, + Tooltip, + Flex, +} from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; +import { useState, useMemo, useId } from '@wordpress/element'; +import { + BlockPreview, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { Icon, lockSmall } from '@wordpress/icons'; +import { parse } from '@wordpress/blocks'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import { Async } from '../async'; +import { + PATTERN_TYPES, + TEMPLATE_PART_POST_TYPE, + PATTERN_SYNC_TYPES, + OPERATOR_IS, +} from '../../utils/constants'; +import { unlock } from '../../lock-unlock'; +import { useLink } from '../routes/link'; +import { useAddedBy } from '../page-templates/hooks'; +import { defaultGetTitle } from './search-items'; + +const { useGlobalStyle } = unlock( blockEditorPrivateApis ); + +function PreviewWrapper( { item, onClick, ariaDescribedBy, children } ) { + return ( + + ); +} + +function PreviewField( { item } ) { + const descriptionId = useId(); + const description = item.description || item?.excerpt?.raw; + const isUserPattern = item.type === PATTERN_TYPES.user; + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const [ backgroundColor ] = useGlobalStyle( 'color.background' ); + const { onClick } = useLink( { + postType: item.type, + postId: isUserPattern || isTemplatePart ? item.id : item.name, + canvas: 'edit', + } ); + const blocks = useMemo( () => { + return ( + item.blocks ?? + parse( item.content.raw, { + __unstableSkipMigrationLogs: true, + } ) + ); + }, [ item?.content?.raw, item.blocks ] ); + const isEmpty = ! blocks?.length; + + return ( +
+ + { isEmpty && isTemplatePart && __( 'Empty template part' ) } + { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } + { ! isEmpty && ( + + + + ) } + + { !! description && ( + + ) } +
+ ); +} + +export const previewField = { + label: __( 'Preview' ), + id: 'preview', + render: PreviewField, + enableSorting: false, +}; + +function TitleField( { item } ) { + const isUserPattern = item.type === PATTERN_TYPES.user; + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const { onClick } = useLink( { + postType: item.type, + postId: isUserPattern || isTemplatePart ? item.id : item.name, + canvas: 'edit', + } ); + const title = decodeEntities( defaultGetTitle( item ) ); + return ( + + + { item.type === PATTERN_TYPES.theme ? ( + title + ) : ( + + ) } + + { item.type === PATTERN_TYPES.theme && ( + + + + ) } + + ); +} + +export const titleField = { + label: __( 'Title' ), + id: 'title', + getValue: ( { item } ) => item.title?.raw || item.title, + render: TitleField, + enableHiding: false, +}; + +const SYNC_FILTERS = [ + { + value: PATTERN_SYNC_TYPES.full, + label: _x( 'Synced', 'pattern (singular)' ), + description: __( 'Patterns that are kept in sync across the site.' ), + }, + { + value: PATTERN_SYNC_TYPES.unsynced, + label: _x( 'Not synced', 'pattern (singular)' ), + description: __( + 'Patterns that can be changed freely without affecting the site.' + ), + }, +]; + +export const patternStatusField = { + label: __( 'Sync status' ), + id: 'sync-status', + render: ( { item } ) => { + const syncStatus = + 'wp_pattern_sync_status' in item + ? item.wp_pattern_sync_status || PATTERN_SYNC_TYPES.full + : PATTERN_SYNC_TYPES.unsynced; + // User patterns can have their sync statuses checked directly. + // Non-user patterns are all unsynced for the time being. + return ( + + { + SYNC_FILTERS.find( ( { value } ) => value === syncStatus ) + .label + } + + ); + }, + elements: SYNC_FILTERS, + filterBy: { + operators: [ OPERATOR_IS ], + isPrimary: true, + }, + enableSorting: false, +}; + +function AuthorField( { item } ) { + const [ isImageLoaded, setIsImageLoaded ] = useState( false ); + const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + + return ( + + { imageUrl && ( +
+ setIsImageLoaded( true ) } + alt="" + src={ imageUrl } + /> +
+ ) } + { ! imageUrl && ( +
+ +
+ ) } + { text } +
+ ); +} + +export const templatePartAuthorField = { + label: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item.author_text, + render: AuthorField, + filterBy: { + isPrimary: true, + }, +}; diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index a0f107e91601bd..a8db73272c0ce7 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -1,59 +1,39 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ -import { - __experimentalHStack as HStack, - Button, - Tooltip, - Flex, -} from '@wordpress/components'; -import { __, _x } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { useState, useMemo, useId, useEffect } from '@wordpress/element'; -import { - BlockPreview, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; -import { Icon, lockSmall } from '@wordpress/icons'; import { usePrevious } from '@wordpress/compose'; import { useEntityRecords } from '@wordpress/core-data'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { parse } from '@wordpress/blocks'; -import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies */ -import { Async } from '../async'; import Page from '../page'; import { LAYOUT_GRID, LAYOUT_TABLE, - LAYOUT_LIST, PATTERN_TYPES, TEMPLATE_PART_POST_TYPE, - PATTERN_SYNC_TYPES, PATTERN_DEFAULT_CATEGORY, - OPERATOR_IS, } from '../../utils/constants'; import usePatternSettings from './use-pattern-settings'; import { unlock } from '../../lock-unlock'; import usePatterns from './use-patterns'; import PatternsHeader from './header'; -import { useLink } from '../routes/link'; -import { useAddedBy } from '../page-templates/hooks'; import { useEditPostAction } from '../dataviews-actions'; -import { defaultGetTitle } from './search-items'; +import { + patternStatusField, + previewField, + titleField, + templatePartAuthorField, +} from './fields'; -const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( - blockEditorPrivateApis -); +const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { usePostActions } = unlock( editorPrivateApis ); const { useLocation } = unlock( routerPrivateApis ); @@ -90,164 +70,6 @@ const DEFAULT_VIEW = { filters: [], }; -const SYNC_FILTERS = [ - { - value: PATTERN_SYNC_TYPES.full, - label: _x( 'Synced', 'pattern (singular)' ), - description: __( 'Patterns that are kept in sync across the site.' ), - }, - { - value: PATTERN_SYNC_TYPES.unsynced, - label: _x( 'Not synced', 'pattern (singular)' ), - description: __( - 'Patterns that can be changed freely without affecting the site.' - ), - }, -]; - -function PreviewWrapper( { item, onClick, ariaDescribedBy, children } ) { - return ( - - ); -} - -function Preview( { item, viewType } ) { - const descriptionId = useId(); - const description = item.description || item?.excerpt?.raw; - const isUserPattern = item.type === PATTERN_TYPES.user; - const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; - const [ backgroundColor ] = useGlobalStyle( 'color.background' ); - const { onClick } = useLink( { - postType: item.type, - postId: isUserPattern || isTemplatePart ? item.id : item.name, - canvas: 'edit', - } ); - const blocks = useMemo( () => { - return ( - item.blocks ?? - parse( item.content.raw, { - __unstableSkipMigrationLogs: true, - } ) - ); - }, [ item?.content?.raw, item.blocks ] ); - const isEmpty = ! blocks?.length; - - return ( -
- - { isEmpty && isTemplatePart && __( 'Empty template part' ) } - { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } - { ! isEmpty && ( - - - - ) } - - { !! description && ( - - ) } -
- ); -} - -function Author( { item, viewType } ) { - const [ isImageLoaded, setIsImageLoaded ] = useState( false ); - const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); - const withIcon = viewType !== LAYOUT_LIST; - - return ( - - { withIcon && imageUrl && ( -
- setIsImageLoaded( true ) } - alt="" - src={ imageUrl } - /> -
- ) } - { withIcon && ! imageUrl && ( -
- -
- ) } - { text } -
- ); -} - -function Title( { item } ) { - const isUserPattern = item.type === PATTERN_TYPES.user; - const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; - const { onClick } = useLink( { - postType: item.type, - postId: isUserPattern || isTemplatePart ? item.id : item.name, - canvas: 'edit', - } ); - const title = decodeEntities( defaultGetTitle( item ) ); - return ( - - - { item.type === PATTERN_TYPES.theme ? ( - title - ) : ( - - ) } - - { item.type === PATTERN_TYPES.theme && ( - - - - ) } - - ); -} - export default function DataviewsPatterns() { const { params: { postType, categoryId: categoryIdFromURL }, @@ -267,6 +89,7 @@ export default function DataviewsPatterns() { const { records } = useEntityRecords( 'postType', TEMPLATE_PART_POST_TYPE, { per_page: -1, } ); + const authors = useMemo( () => { if ( ! records ) { return EMPTY_ARRAY; @@ -282,72 +105,19 @@ export default function DataviewsPatterns() { }, [ records ] ); const fields = useMemo( () => { - const _fields = [ - { - label: __( 'Preview' ), - id: 'preview', - render: ( { item } ) => ( - - ), - enableSorting: false, - }, - { - label: __( 'Title' ), - id: 'title', - getValue: ( { item } ) => item.title?.raw || item.title, - render: ( { item } ) => , - enableHiding: false, - }, - ]; + const _fields = [ previewField, titleField ]; if ( type === PATTERN_TYPES.user ) { - _fields.push( { - label: __( 'Sync status' ), - id: 'sync-status', - render: ( { item } ) => { - const syncStatus = - 'wp_pattern_sync_status' in item - ? item.wp_pattern_sync_status || - PATTERN_SYNC_TYPES.full - : PATTERN_SYNC_TYPES.unsynced; - // User patterns can have their sync statuses checked directly. - // Non-user patterns are all unsynced for the time being. - return ( - <span - className={ `edit-site-patterns__field-sync-status-${ syncStatus }` } - > - { - SYNC_FILTERS.find( - ( { value } ) => value === syncStatus - ).label - } - </span> - ); - }, - elements: SYNC_FILTERS, - filterBy: { - operators: [ OPERATOR_IS ], - isPrimary: true, - }, - enableSorting: false, - } ); + _fields.push( patternStatusField ); } else if ( type === TEMPLATE_PART_POST_TYPE ) { _fields.push( { - label: __( 'Author' ), - id: 'author', - getValue: ( { item } ) => item.author_text, - render: ( { item } ) => { - return <Author viewType={ view.type } item={ item } />; - }, + ...templatePartAuthorField, elements: authors, - filterBy: { - isPrimary: true, - }, } ); } return _fields; - }, [ view.type, type, authors ] ); + }, [ type, authors ] ); // Reset the page number when the category changes. useEffect( () => { diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 2014d3b94fdae3..f457624d100c1f 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -1,118 +1,114 @@ -.edit-site-patterns__section-header { - .screen-reader-shortcut:focus { - top: 0; - } +.edit-site-patterns__delete-modal { + width: $modal-width-small; } -.edit-site-patterns__pattern-title { - .is-link { - text-decoration: none; - color: $gray-200; +.page-patterns-preview-field { + display: flex; + flex-direction: column; + height: 100%; - &:hover, - &:focus { - color: $white; + .dataviews-view-table & { + width: 96px; + flex-grow: 0; + border-radius: 2px; + + .page-patterns-preview-field__button { + border-radius: 2px; } } - .edit-site-patterns__pattern-icon { + .page-patterns-preview-field__button { + box-shadow: none; + border: none; + padding: 0; + background-color: unset; + box-sizing: border-box; + cursor: pointer; + overflow: hidden; + height: 100%; border-radius: $grid-unit-05; - background: var(--wp-block-synced-color); - fill: $white; - } - .edit-site-patterns__pattern-lock-icon { - fill: currentcolor; + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + } + + &[aria-disabled="true"] { + cursor: default; + } } } -.edit-site-patterns__delete-modal { - width: $modal-width-small; +.edit-site-patterns__pattern-icon { + fill: var(--wp-block-synced-color); + flex-shrink: 0; } -.edit-site-page-patterns-dataviews { - .page-patterns-preview-field { - display: flex; - flex-direction: column; - height: 100%; - - &.is-viewtype-table { - width: 96px; - flex-grow: 0; - border-radius: 2px; +.edit-site-patterns__pattern-lock-icon { + min-width: min-content; +} - .page-patterns-preview-field__button { - border-radius: 2px; - } - } +.edit-site-patterns__section-header { + border-bottom: 1px solid #f0f0f0; + padding: $grid-unit-20 $grid-unit-60; + position: sticky; + top: 0; + z-index: 2; + flex-shrink: 0; + transition: padding ease-out 0.1s; + @include reduce-motion("transition"); + min-height: $grid-unit-50; + + .edit-site-patterns__title { + min-height: $grid-unit-50; - .page-patterns-preview-field__button { - box-shadow: none; - border: none; - padding: 0; - background-color: unset; - box-sizing: border-box; - cursor: pointer; - overflow: hidden; - height: 100%; - border-radius: $grid-unit-05; - - &:focus-visible { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - } - - &[aria-disabled="true"] { - cursor: default; - } + .components-heading { + flex-grow: 1; + flex-basis: 0; + white-space: nowrap; } } - .edit-site-patterns__pattern-icon { - fill: var(--wp-block-synced-color); - flex-shrink: 0; + .edit-site-patterns__sub-title { + margin-bottom: $grid-unit-10; } - .edit-site-patterns__pattern-lock-icon { - min-width: min-content; + .screen-reader-shortcut:focus { + top: 0; } +} - .edit-site-patterns__section-header { - border-bottom: 1px solid #f0f0f0; - padding: $grid-unit-20 $grid-unit-60; - position: sticky; - top: 0; - z-index: 2; - flex-shrink: 0; - transition: padding ease-out 0.1s; - @include reduce-motion("transition"); - min-height: $grid-unit-50; +.edit-site-patterns__pattern-title { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: inherit; - .edit-site-patterns__title { - min-height: $grid-unit-50; + .is-link { + text-decoration: none; + color: $gray-200; - .components-heading { - flex-grow: 1; - flex-basis: 0; - white-space: nowrap; - } + &:hover, + &:focus { + color: $white; } + } - .edit-site-patterns__sub-title { - margin-bottom: $grid-unit-10; - } + .edit-site-patterns__pattern-icon { + border-radius: $grid-unit-05; + background: var(--wp-block-synced-color); + fill: $white; } - .edit-site-patterns__pattern-title { - display: block; - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: inherit; + .edit-site-patterns__pattern-lock-icon { + fill: currentcolor; } +} +.edit-site-page-patterns-dataviews { .dataviews-pagination { z-index: z-index(".edit-site-patterns__dataviews-list-pagination"); } diff --git a/packages/edit-site/src/components/page-templates/fields.js b/packages/edit-site/src/components/page-templates/fields.js new file mode 100644 index 00000000000000..d26f1906a10664 --- /dev/null +++ b/packages/edit-site/src/components/page-templates/fields.js @@ -0,0 +1,157 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { Icon, __experimentalHStack as HStack } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; +import { parse } from '@wordpress/blocks'; +import { + BlockPreview, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { EditorProvider } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import { Async } from '../async'; +import { default as Link, useLink } from '../routes/link'; +import { useAddedBy } from './hooks'; + +import usePatternSettings from '../page-patterns/use-pattern-settings'; +import { unlock } from '../../lock-unlock'; + +const { useGlobalStyle } = unlock( blockEditorPrivateApis ); + +function PreviewField( { item } ) { + const settings = usePatternSettings(); + const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); + const blocks = useMemo( () => { + return parse( item.content.raw ); + }, [ item.content.raw ] ); + const { onClick } = useLink( { + postId: item.id, + postType: item.type, + canvas: 'edit', + } ); + + const isEmpty = ! blocks?.length; + // Wrap everything in a block editor provider to ensure 'styles' that are needed + // for the previews are synced between the site editor store and the block editor store. + // Additionally we need to have the `__experimentalBlockPatterns` setting in order to + // render patterns inside the previews. + // TODO: Same approach is used in the patterns list and it becomes obvious that some of + // the block editor settings are needed in context where we don't have the block editor. + // Explore how we can solve this in a better way. + return ( + <EditorProvider post={ item } settings={ settings }> + <div + className="page-templates-preview-field" + style={ { backgroundColor } } + > + <button + className="page-templates-preview-field__button" + type="button" + onClick={ onClick } + aria-label={ item.title?.rendered || item.title } + > + { isEmpty && __( 'Empty template' ) } + { ! isEmpty && ( + <Async> + <BlockPreview blocks={ blocks } /> + </Async> + ) } + </button> + </div> + </EditorProvider> + ); +} + +export const previewField = { + label: __( 'Preview' ), + id: 'preview', + render: PreviewField, + enableSorting: false, +}; + +function TitleField( { item } ) { + const linkProps = { + params: { + postId: item.id, + postType: item.type, + canvas: 'edit', + }, + }; + return ( + <Link { ...linkProps }> + { decodeEntities( item.title?.rendered ) || __( '(no title)' ) } + </Link> + ); +} + +export const titleField = { + label: __( 'Template' ), + id: 'title', + getValue: ( { item } ) => item.title?.rendered, + render: TitleField, + enableHiding: false, + enableGlobalSearch: true, +}; + +export const descriptionField = { + label: __( 'Description' ), + id: 'description', + render: ( { item } ) => { + return ( + item.description && ( + <span className="page-templates-description"> + { decodeEntities( item.description ) } + </span> + ) + ); + }, + enableSorting: false, + enableGlobalSearch: true, +}; + +function AuthorField( { item } ) { + const [ isImageLoaded, setIsImageLoaded ] = useState( false ); + const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + + return ( + <HStack alignment="left" spacing={ 0 }> + { imageUrl && ( + <div + className={ clsx( 'page-templates-author-field__avatar', { + 'is-loaded': isImageLoaded, + } ) } + > + <img + onLoad={ () => setIsImageLoaded( true ) } + alt="" + src={ imageUrl } + /> + </div> + ) } + { ! imageUrl && ( + <div className="page-templates-author-field__icon"> + <Icon icon={ icon } /> + </div> + ) } + <span className="page-templates-author-field__name">{ text }</span> + </HStack> + ); +} + +export const authorField = { + label: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item.author_text, + render: AuthorField, +}; diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 9ec87ee8cca352..9c5db8fb1699d5 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -1,36 +1,18 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ -import { Icon, __experimentalHStack as HStack } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; -import { decodeEntities } from '@wordpress/html-entities'; -import { parse } from '@wordpress/blocks'; -import { - BlockPreview, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { - privateApis as editorPrivateApis, - EditorProvider, -} from '@wordpress/editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ -import { Async } from '../async'; import Page from '../page'; -import { default as Link, useLink } from '../routes/link'; import AddNewTemplate from '../add-new-template'; -import { useAddedBy } from './hooks'; import { TEMPLATE_POST_TYPE, OPERATOR_IS_ANY, @@ -38,14 +20,16 @@ import { LAYOUT_TABLE, LAYOUT_LIST, } from '../../utils/constants'; - -import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; import { useEditPostAction } from '../dataviews-actions'; +import { + authorField, + descriptionField, + previewField, + titleField, +} from './fields'; const { usePostActions } = unlock( editorPrivateApis ); - -const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const { useHistory, useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; @@ -109,104 +93,6 @@ const DEFAULT_VIEW = { filters: [], }; -function Title( { item, viewType } ) { - if ( viewType === LAYOUT_LIST ) { - return decodeEntities( item.title?.rendered ) || __( '(no title)' ); - } - const linkProps = { - params: { - postId: item.id, - postType: item.type, - canvas: 'edit', - }, - }; - return ( - <Link { ...linkProps }> - { decodeEntities( item.title?.rendered ) || __( '(no title)' ) } - </Link> - ); -} - -function AuthorField( { item } ) { - const [ isImageLoaded, setIsImageLoaded ] = useState( false ); - const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); - - return ( - <HStack alignment="left" spacing={ 0 }> - { imageUrl && ( - <div - className={ clsx( 'page-templates-author-field__avatar', { - 'is-loaded': isImageLoaded, - } ) } - > - <img - onLoad={ () => setIsImageLoaded( true ) } - alt="" - src={ imageUrl } - /> - </div> - ) } - { ! imageUrl && ( - <div className="page-templates-author-field__icon"> - <Icon icon={ icon } /> - </div> - ) } - <span className="page-templates-author-field__name">{ text }</span> - </HStack> - ); -} - -function Preview( { item, viewType } ) { - const settings = usePatternSettings(); - const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); - const blocks = useMemo( () => { - return parse( item.content.raw ); - }, [ item.content.raw ] ); - const { onClick } = useLink( { - postId: item.id, - postType: item.type, - canvas: 'edit', - } ); - - const isEmpty = ! blocks?.length; - // Wrap everything in a block editor provider to ensure 'styles' that are needed - // for the previews are synced between the site editor store and the block editor store. - // Additionally we need to have the `__experimentalBlockPatterns` setting in order to - // render patterns inside the previews. - // TODO: Same approach is used in the patterns list and it becomes obvious that some of - // the block editor settings are needed in context where we don't have the block editor. - // Explore how we can solve this in a better way. - return ( - <EditorProvider post={ item } settings={ settings }> - <div - className={ `page-templates-preview-field is-viewtype-${ viewType }` } - style={ { backgroundColor } } - > - { viewType === LAYOUT_LIST && ! isEmpty && ( - <Async> - <BlockPreview blocks={ blocks } /> - </Async> - ) } - { viewType !== LAYOUT_LIST && ( - <button - className="page-templates-preview-field__button" - type="button" - onClick={ onClick } - aria-label={ item.title?.rendered || item.title } - > - { isEmpty && __( 'Empty template' ) } - { ! isEmpty && ( - <Async> - <BlockPreview blocks={ blocks } /> - </Async> - ) } - </button> - ) } - </div> - </EditorProvider> - ); -} - export default function PageTemplates() { const { params } = useLocation(); const { activeView = 'all', layout, postId } = params; @@ -285,50 +171,15 @@ export default function PageTemplates() { const fields = useMemo( () => [ + previewField, + titleField, + descriptionField, { - label: __( 'Preview' ), - id: 'preview', - render: ( { item } ) => { - return <Preview item={ item } viewType={ view.type } />; - }, - enableSorting: false, - }, - { - label: __( 'Template' ), - id: 'title', - getValue: ( { item } ) => item.title?.rendered, - render: ( { item } ) => ( - <Title item={ item } viewType={ view.type } /> - ), - enableHiding: false, - enableGlobalSearch: true, - }, - { - label: __( 'Description' ), - id: 'description', - render: ( { item } ) => { - return ( - item.description && ( - <span className="page-templates-description"> - { decodeEntities( item.description ) } - </span> - ) - ); - }, - enableSorting: false, - enableGlobalSearch: true, - }, - { - label: __( 'Author' ), - id: 'author', - getValue: ( { item } ) => item.author_text, - render: ( { item } ) => { - return <AuthorField viewType={ view.type } item={ item } />; - }, + ...authorField, elements: authors, }, ], - [ authors, view.type ] + [ authors ] ); const { data, paginationInfo } = useMemo( () => { diff --git a/packages/edit-site/src/components/page-templates/style.scss b/packages/edit-site/src/components/page-templates/style.scss index 725a3c4f89746c..f3d596cc90f18e 100644 --- a/packages/edit-site/src/components/page-templates/style.scss +++ b/packages/edit-site/src/components/page-templates/style.scss @@ -22,19 +22,19 @@ } } - &.is-viewtype-list { + .dataviews-view-list & { .block-editor-block-preview__container { height: 120px; } } - &.is-viewtype-grid { + .dataviews-view-grid & { .block-editor-block-preview__container { height: 100%; } } - &.is-viewtype-table { + .dataviews-view-table & { border-radius: $radius-block-ui; position: relative; diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index ff25fbcd1962fb..68b8461929eb1d 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -2,7 +2,10 @@ * WordPress dependencies */ import { Button } from '@wordpress/components'; -import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; +import { + store as coreStore, + privateApis as coreDataPrivateApis, +} from '@wordpress/core-data'; import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -33,6 +36,7 @@ import usePostFields from '../post-fields'; const { usePostActions } = unlock( editorPrivateApis ); const { useLocation, useHistory } = unlock( routerPrivateApis ); +const { useEntityRecordsWithPermissions } = unlock( coreDataPrivateApis ); const EMPTY_ARRAY = []; function useView( postType ) { @@ -199,7 +203,7 @@ export default function PostList( { postType } ) { isResolving: isLoadingMainEntities, totalItems, totalPages, - } = useEntityRecords( 'postType', postType, queryArgs ); + } = useEntityRecordsWithPermissions( 'postType', postType, queryArgs ); const ids = records?.map( ( record ) => getItemId( record ) ) ?? []; const prevIds = usePrevious( ids ) ?? []; diff --git a/packages/editor/src/components/editor-interface/style.scss b/packages/editor/src/components/editor-interface/style.scss index 77f621acc93510..05b23fe2304dd8 100644 --- a/packages/editor/src/components/editor-interface/style.scss +++ b/packages/editor/src/components/editor-interface/style.scss @@ -2,6 +2,11 @@ height: $header-height + $border-width; } +.editor-editor-interface .interface-interface-skeleton__content { + // Make this a stacking context to contain the z-index of children elements. + isolation: isolate; +} + .editor-visual-editor { flex: 1 0 auto; } diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 2a97e688eeedbe..190b8ea6ca32f5 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -11,14 +11,13 @@ import { store as noticesStore } from '@wordpress/notices'; import { useMemo, useState } from '@wordpress/element'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { parse } from '@wordpress/blocks'; -import { DataForm } from '@wordpress/dataviews'; +import { DataForm, isItemValid } from '@wordpress/dataviews'; import { Button, TextControl, __experimentalText as Text, __experimentalHStack as HStack, __experimentalVStack as VStack, - __experimentalNumberControl as NumberControl, } from '@wordpress/components'; /** @@ -39,21 +38,31 @@ import { getItemTitle } from '../../dataviews/actions/utils'; const { PATTERN_TYPES, CreatePatternModalContents, useDuplicatePatternProps } = unlock( patternsPrivateApis ); -// TODO: this should be shared with other components (page-pages). +// TODO: this should be shared with other components (see post-fields in edit-site). const fields = [ { type: 'text', - header: __( 'Title' ), id: 'title', + label: __( 'Title' ), placeholder: __( 'No title' ), getValue: ( { item } ) => item.title, }, + { + type: 'integer', + id: 'menu_order', + label: __( 'Order' ), + description: __( 'Determines the order of pages.' ), + }, ]; -const form = { +const formDuplicateAction = { visibleFields: [ 'title' ], }; +const formOrderAction = { + visibleFields: [ 'menu_order' ], +}; + /** * Check if a template is removable. * @@ -635,21 +644,20 @@ function useRenamePostAction( postType ) { } function ReorderModal( { items, closeModal, onActionPerformed } ) { - const [ item ] = items; + const [ item, setItem ] = useState( items[ 0 ] ); + const orderInput = item.menu_order; const { editEntityRecord, saveEditedEntityRecord } = useDispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); - const [ orderInput, setOrderInput ] = useState( item.menu_order ); async function onOrder( event ) { event.preventDefault(); - if ( - ! Number.isInteger( Number( orderInput ) ) || - orderInput?.trim?.() === '' - ) { + + if ( ! isItemValid( item, fields, formOrderAction ) ) { return; } + try { await editEntityRecord( 'postType', item.type, item.id, { menu_order: orderInput, @@ -673,9 +681,7 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) { } ); } } - const saveIsDisabled = - ! Number.isInteger( Number( orderInput ) ) || - orderInput?.trim?.() === ''; + const isSaveDisabled = ! isItemValid( item, fields, formOrderAction ); return ( <form onSubmit={ onOrder }> <VStack spacing="5"> @@ -684,12 +690,11 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) { 'Determines the order of pages. Pages with the same order value are sorted alphabetically. Negative order values are supported.' ) } </div> - <NumberControl - __next40pxDefaultSize - label={ __( 'Order' ) } - help={ __( 'Set the page order.' ) } - value={ orderInput } - onChange={ setOrderInput } + <DataForm + data={ item } + fields={ fields } + form={ formOrderAction } + onChange={ setItem } /> <HStack justify="right"> <Button @@ -706,7 +711,7 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) { variant="primary" type="submit" accessibleWhenDisabled - disabled={ saveIsDisabled } + disabled={ isSaveDisabled } __experimentalIsFocusable > { __( 'Save' ) } @@ -873,7 +878,7 @@ const useDuplicatePostAction = ( postType ) => { <DataForm data={ item } fields={ fields } - form={ form } + form={ formDuplicateAction } onChange={ setItem } /> <HStack spacing={ 2 } justify="end"> diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js index 35b060b096c069..9abb0e14079d5e 100644 --- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -45,8 +45,8 @@ export default function DisableNonPageContentBlocks() { ); const disabledIds = useSelect( ( select ) => { const { getBlocksByName, getBlockOrder } = select( blockEditorStore ); - return getBlocksByName( [ 'core/template-part' ] ).flatMap( - ( clientId ) => getBlockOrder( clientId ) + return getBlocksByName( 'core/template-part' ).flatMap( ( clientId ) => + getBlockOrder( clientId ) ); }, [] ); diff --git a/packages/editor/src/components/visual-editor/style.scss b/packages/editor/src/components/visual-editor/style.scss index 2d7ed665a957f3..18a61827d573da 100644 --- a/packages/editor/src/components/visual-editor/style.scss +++ b/packages/editor/src/components/visual-editor/style.scss @@ -2,8 +2,6 @@ position: relative; display: block; background-color: $gray-300; - // Make this a stacking context to contain the z-index of children elements. - isolation: isolate; // Centralize the editor horizontally (flex-direction is column). align-items: center; diff --git a/schemas/json/wp-env.json b/schemas/json/wp-env.json index 9208ff8e0661f2..491d1f8cf73017 100644 --- a/schemas/json/wp-env.json +++ b/schemas/json/wp-env.json @@ -82,6 +82,7 @@ "$ref": "#/definitions/wpEnvProperties" }, { + "type": "object", "properties": { "$schema": { "type": "string" @@ -91,10 +92,10 @@ "type": "object", "patternProperties": { "[a-zA-Z]": { - "type": "object", "allOf": [ { "$ref": "#/definitions/wpEnvProperties" }, { + "type": "object", "propertyNames": { "$ref": "#/definitions/wpEnvPropertyNames" } @@ -107,6 +108,7 @@ } }, { + "type": "object", "propertyNames": { "anyOf": [ { diff --git a/test/e2e/specs/editor/various/is-typing.spec.js b/test/e2e/specs/editor/various/is-typing.spec.js index e2c65f01928e04..10ac90d1084127 100644 --- a/test/e2e/specs/editor/various/is-typing.spec.js +++ b/test/e2e/specs/editor/various/is-typing.spec.js @@ -54,7 +54,7 @@ test.describe( 'isTyping', () => { .click(); await editor.openDocumentSettingsSidebar(); - await page.getByLabel( 'Inherit query from template' ).click(); + await page.getByLabel( 'Custom' ).click(); // Moving the mouse shows the toolbar. await editor.showBlockToolbar(); diff --git a/test/integration/wp-env-schema.test.js b/test/integration/wp-env-schema.test.js new file mode 100644 index 00000000000000..48ef039a8fb0d5 --- /dev/null +++ b/test/integration/wp-env-schema.test.js @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import Ajv from 'ajv'; + +/** + * Internal dependencies + */ +import wpEnvSchema from '../../schemas/json/wp-env.json'; +import wpEnvJsonFile from '../../.wp-env.json'; + +describe( '.wp-env.json schema', () => { + const ajv = new Ajv( { + allowMatchingProperties: true, + } ); + + test( 'strictly adheres to the draft-07 meta schema', () => { + // Use ajv.compile instead of ajv.validateSchema to validate the schema + // because validateSchema only checks syntax, whereas, compile checks + // if the schema is semantically correct with strict mode. + // See https://github.com/ajv-validator/ajv/issues/1434#issuecomment-822982571 + const result = ajv.compile( wpEnvSchema ); + + expect( result.errors ).toBe( null ); + } ); + + test( 'validates schema for .wp-env.json', () => { + // We want to validate the .wp-env.json file using the local schema. + const { $schema, ...metadata } = wpEnvJsonFile; + + // we expect the $schema property to be present in the .wp-env.json file + expect( $schema ).toBeTruthy(); + + const result = ajv.validate( wpEnvSchema, metadata ) || ajv.errors; + + expect( result ).toBe( true ); + } ); +} );