Refactor View Switcher to use Block Attributes (https://github.com/woocommerce/woocommerce-blocks/pull/8006)

* Add cart view switcher

* Attribute based switched POC

* Tidy up view handling

* Mini cart

* Not sure who clint is. Typo - rename to clientId

* Avoid string casting in TS

* Add margin to title

* Set custom source to prevent currentView saving to post content.

Note this also removes `save` which does not exist in Gutenberg.

* Remove higher order component from withViewSwitcher

* Import view switcher in main file

* Add to side effects

* Move view switcher import

Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
This commit is contained in:
Mike Jolley 2022-12-22 14:35:24 +00:00 committed by GitHub
parent ece76603bb
commit 637f47cac7
17 changed files with 330 additions and 347 deletions

View File

@ -1,82 +0,0 @@
/**
* External dependencies
*/
import { createContext, useContext, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
/**
* @typedef {import('@woocommerce/type-defs/contexts').EditorDataContext} EditorDataContext
* @typedef {import('@woocommerce/type-defs/cart').CartData} CartData
*/
const EditorContext = createContext( {
isEditor: false,
currentPostId: 0,
currentView: '',
previewData: {},
getPreviewData: () => void null,
} );
/**
* @return {EditorDataContext} Returns the editor data context value
*/
export const useEditorContext = () => {
return useContext( EditorContext );
};
/**
* Editor provider
*
* @param {Object} props Incoming props for the provider.
* @param {*} props.children The children being wrapped.
* @param {Object} [props.previewData] The preview data for editor.
* @param {number} [props.currentPostId] The post being edited.
* @param {string} [props.currentView] Current view, if using a view switcher.
*/
export const EditorProvider = ( {
children,
currentPostId = 0,
currentView = '',
previewData = {},
} ) => {
/**
* @type {number} editingPostId
*/
const editingPostId = useSelect(
( select ) => {
if ( ! currentPostId ) {
const store = select( 'core/editor' );
return store.getCurrentPostId();
}
return currentPostId;
},
[ currentPostId ]
);
const getPreviewData = useCallback(
( name ) => {
if ( name in previewData ) {
return previewData[ name ];
}
return {};
},
[ previewData ]
);
/**
* @type {EditorDataContext}
*/
const editorData = {
isEditor: true,
currentPostId: editingPostId,
currentView,
previewData,
getPreviewData,
};
return (
<EditorContext.Provider value={ editorData }>
{ children }
</EditorContext.Provider>
);
};

View File

@ -0,0 +1,78 @@
/**
* External dependencies
*/
import { createContext, useContext, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
interface EditorContextType {
// Indicates whether in the editor context.
isEditor: boolean;
// The post ID being edited.
currentPostId: number;
// The current view name, if using a view switcher.
currentView: string;
// Object containing preview data for the editor.
previewData: Record< string, unknown >;
// Get data by name.
getPreviewData: ( name: string ) => Record< string, unknown >;
}
const EditorContext = createContext( {
isEditor: false,
currentPostId: 0,
currentView: '',
previewData: {},
getPreviewData: () => ( {} ),
} as EditorContextType );
export const useEditorContext = (): EditorContextType => {
return useContext( EditorContext );
};
export const EditorProvider = ( {
children,
currentPostId = 0,
previewData = {},
currentView = '',
}: {
children: React.ReactChildren;
currentPostId?: number | undefined;
previewData?: Record< string, unknown > | undefined;
currentView?: string | undefined;
} ) => {
const editingPostId = useSelect(
( select ): number =>
currentPostId
? currentPostId
: select( 'core/editor' ).getCurrentPostId(),
[ currentPostId ]
);
const getPreviewData = useCallback(
( name: string ): Record< string, unknown > => {
if ( previewData && name in previewData ) {
return previewData[ name ] as Record< string, unknown >;
}
return {};
},
[ previewData ]
);
const editorData: EditorContextType = {
isEditor: true,
currentPostId: editingPostId,
currentView,
previewData,
getPreviewData,
};
return (
<EditorContext.Provider value={ editorData }>
{ children }
</EditorContext.Provider>
);
};

View File

@ -1,6 +1,5 @@
export * from './hacks';
export * from './use-forced-layout';
export * from './editor-utils';
export * from './use-view-switcher';
export * from './sidebar-notices';
export * from './block-settings';

View File

@ -1,111 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { useDispatch, select } from '@wordpress/data';
import { ToolbarGroup, ToolbarDropdownMenu } from '@wordpress/components';
import { eye } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { store as blockEditorStore } from '@wordpress/block-editor';
interface View {
view: string;
label: string;
icon: string | JSX.Element;
}
function getView( viewName: string, views: View[] ) {
return views.find( ( view ) => view.view === viewName );
}
export const useViewSwitcher = (
clientId: string,
views: View[]
): {
currentView: string;
component: JSX.Element;
} => {
const initialView = views[ 0 ];
const [ currentView, setCurrentView ] = useState( initialView );
const { selectBlock } = useDispatch( 'core/block-editor' );
const { getBlock, getSelectedBlockClientId, getBlockParentsByBlockName } =
select( blockEditorStore );
const selectedBlockClientId = getSelectedBlockClientId();
useEffect( () => {
const selectedBlock = getBlock( selectedBlockClientId );
if ( ! selectedBlock ) {
return;
}
if ( currentView.view === selectedBlock.name ) {
return;
}
const viewNames = views.map( ( { view } ) => view );
if ( viewNames.includes( selectedBlock.name ) ) {
const newView = getView( selectedBlock.name, views );
if ( newView ) {
return setCurrentView( newView );
}
}
const parentBlockIds = getBlockParentsByBlockName(
selectedBlockClientId,
viewNames
);
if ( parentBlockIds.length !== 1 ) {
return;
}
const parentBlock = getBlock( parentBlockIds[ 0 ] );
if ( currentView.view === parentBlock.name ) {
return;
}
const newView = getView( parentBlock.name, views );
if ( newView ) {
setCurrentView( newView );
}
}, [
getBlockParentsByBlockName,
selectedBlockClientId,
getBlock,
currentView.view,
views,
] );
const ViewSwitcherComponent = (
<ToolbarGroup>
<ToolbarDropdownMenu
label={ __( 'Switch view', 'woo-gutenberg-products-block' ) }
text={ currentView.label }
icon={ <Icon icon={ eye } style={ { marginRight: '8px' } } /> }
controls={ views.map( ( view ) => ( {
...view,
title: <span>{ view.label }</span>,
isActive: view.view === currentView.view,
onClick: () => {
setCurrentView( view );
selectBlock(
getBlock( clientId ).innerBlocks.find(
( block: { name: string } ) =>
block.name === view.view
)?.clientId || clientId
);
},
} ) ) }
/>
</ToolbarGroup>
);
return {
currentView: currentView.view,
component: ViewSwitcherComponent,
};
};

View File

@ -0,0 +1,54 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
import { addFilter, hasFilter } from '@wordpress/hooks';
import type { EditorBlock } from '@woocommerce/types';
/**
* Internal dependencies
*/
import Switcher from './switcher';
import { findParentBlockEditorViews } from './utils';
const withViewSwitcher =
< T extends EditorBlock< T > >( BlockEdit: React.ElementType ) =>
( props: Record< string, unknown > ) => {
const { clientId } = props as { clientId: string };
const { views, currentView, viewClientId } = useSelect( ( select ) => {
const blockAttributes =
select( 'core/block-editor' ).getBlockAttributes( clientId );
return blockAttributes?.editorViews
? {
views: blockAttributes.editorViews,
currentView: blockAttributes.currentView,
viewClientId: clientId,
}
: findParentBlockEditorViews( clientId );
} );
if ( views.length === 0 ) {
return <BlockEdit { ...props } />;
}
return (
<>
<Switcher
currentView={ currentView }
views={ views }
clientId={ viewClientId }
/>
<BlockEdit { ...props } />
</>
);
};
if ( ! hasFilter( 'editor.BlockEdit', 'woocommerce/with-view-switcher' ) ) {
addFilter(
'editor.BlockEdit',
'woocommerce/with-view-switcher',
withViewSwitcher,
11
);
}

View File

@ -0,0 +1,71 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useDispatch, select as dataSelect } from '@wordpress/data';
import { ToolbarGroup, ToolbarDropdownMenu } from '@wordpress/components';
import { BlockControls } from '@wordpress/block-editor';
import { Icon } from '@wordpress/icons';
import { eye } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import type { View } from './types';
import { getView } from './utils';
export const Switcher = ( {
currentView,
views,
clientId,
}: {
currentView: string;
views: View[];
clientId: string;
} ): JSX.Element | null => {
const currentViewObject = getView( currentView, views ) || views[ 0 ];
const currentViewLabel = currentViewObject.label;
const { updateBlockAttributes, selectBlock } =
useDispatch( 'core/block-editor' );
return (
<BlockControls>
<ToolbarGroup>
<ToolbarDropdownMenu
label={ __(
'Switch view',
'woo-gutenberg-products-block'
) }
text={ currentViewLabel }
icon={
<Icon icon={ eye } style={ { marginRight: '8px' } } />
}
controls={ views.map( ( view ) => ( {
...view,
title: (
<span style={ { marginLeft: '8px' } }>
{ view.label }
</span>
),
isActive: view.view === currentView,
onClick: () => {
updateBlockAttributes( clientId, {
currentView: view.view,
} );
selectBlock(
dataSelect( 'core/block-editor' )
.getBlock( clientId )
?.innerBlocks.find(
( block: { name: string } ) =>
block.name === view.view
)?.clientId || clientId
);
},
} ) ) }
/>
</ToolbarGroup>
</BlockControls>
);
};
export default Switcher;

View File

@ -0,0 +1,5 @@
export interface View {
view: string;
label: string;
icon: string | JSX.Element;
}

View File

@ -0,0 +1,64 @@
/**
* External dependencies
*/
import { select } from '@wordpress/data';
/**
* Internal dependencies
*/
import type { View } from './types';
export const getView = (
viewName: string,
views: View[]
): View | undefined => {
return views.find( ( view ) => view.view === viewName );
};
const defaultView = {
views: [],
currentView: '',
viewClientId: '',
};
export const findParentBlockEditorViews = (
clientId: string,
maxDepth = 10,
currentDepth = 0
): {
views: View[];
currentView: string;
viewClientId: string;
} => {
const depth = currentDepth + 1;
if ( depth > maxDepth ) {
return defaultView;
}
const { getBlockAttributes, getBlockRootClientId } =
select( 'core/block-editor' );
const rootId = getBlockRootClientId( clientId );
if ( rootId === null || rootId === '' ) {
return defaultView;
}
const rootAttributes = getBlockAttributes( rootId );
if ( ! rootAttributes ) {
return defaultView;
}
if ( rootAttributes.editorViews !== undefined ) {
return {
views: rootAttributes.editorViews,
currentView:
rootAttributes.currentView ||
rootAttributes.editorViews[ 0 ].view,
viewClientId: rootId,
};
}
return findParentBlockEditorViews( rootId, maxDepth, depth );
};

View File

@ -1,14 +1,36 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { getSetting } from '@woocommerce/settings';
import { Icon } from '@wordpress/icons';
import { filledCart, removeCart } from '@woocommerce/icons';
export const blockName = 'woocommerce/cart';
export const blockAttributes = {
isPreview: {
type: 'boolean',
default: false,
save: false,
},
currentView: {
type: 'string',
default: 'woocommerce/filled-cart-block',
source: 'readonly', // custom source to prevent saving to post content
},
editorViews: {
type: 'object',
default: [
{
view: 'woocommerce/filled-cart-block',
label: __( 'Filled Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ filledCart } />,
},
{
view: 'woocommerce/empty-cart-block',
label: __( 'Empty Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ removeCart } />,
},
],
},
hasDarkControls: {
type: 'boolean',

View File

@ -7,14 +7,11 @@ import { __ } from '@wordpress/i18n';
import {
useBlockProps,
InnerBlocks,
BlockControls,
InspectorControls,
} from '@wordpress/block-editor';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { EditorProvider, CartProvider } from '@woocommerce/base-context';
import { previewCart } from '@woocommerce/resource-previews';
import { filledCart, removeCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
/**
* Internal dependencies
@ -23,12 +20,12 @@ import './inner-blocks';
import './editor.scss';
import {
addClassToBody,
useViewSwitcher,
useBlockPropsWithLocking,
useForcedLayout,
BlockSettings,
} from '../cart-checkout-shared';
import '../cart-checkout-shared/sidebar-notices';
import '../cart-checkout-shared/view-switcher';
import { CartBlockContext } from './context';
// This is adds a class to body to signal if the selected block is locked
@ -40,25 +37,8 @@ const ALLOWED_BLOCKS = [
'woocommerce/empty-cart-block',
];
const views = [
{
view: 'woocommerce/filled-cart-block',
label: __( 'Filled Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ filledCart } />,
},
{
view: 'woocommerce/empty-cart-block',
label: __( 'Empty Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ removeCart } />,
},
];
export const Edit = ( { className, attributes, setAttributes, clientId } ) => {
const { hasDarkControls } = attributes;
const { currentView, component: ViewSwitcherComponent } = useViewSwitcher(
clientId,
views
);
const { hasDarkControls, currentView } = attributes;
const defaultTemplate = [
[ 'woocommerce/filled-cart-block', {}, [] ],
[ 'woocommerce/empty-cart-block', {}, [] ],
@ -98,10 +78,9 @@ export const Edit = ( { className, attributes, setAttributes, clientId } ) => {
) }
>
<EditorProvider
currentView={ currentView }
previewData={ { previewCart } }
currentView={ currentView }
>
<BlockControls>{ ViewSwitcherComponent }</BlockControls>
<CartBlockContext.Provider
value={ {
hasDarkControls,

View File

@ -51,7 +51,6 @@ const settings: BlockConfiguration = {
isPreview: {
type: 'boolean',
default: false,
save: false,
},
addToCartBehaviour: {
type: 'string',

View File

@ -3,14 +3,7 @@
* External dependencies
*/
import type { ReactElement } from 'react';
import {
useBlockProps,
InnerBlocks,
BlockControls,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { filledCart, removeCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { EditorProvider } from '@woocommerce/base-context';
import type { TemplateArray } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
@ -18,7 +11,7 @@ import { useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useViewSwitcher, useForcedLayout } from '../../cart-checkout-shared';
import { useForcedLayout } from '../../cart-checkout-shared';
import { MiniCartInnerBlocksStyle } from './inner-blocks-style';
import './editor.scss';
@ -28,24 +21,11 @@ const ALLOWED_BLOCKS = [
'woocommerce/empty-mini-cart-contents-block',
];
const views = [
{
view: 'woocommerce/filled-mini-cart-contents-block',
label: __( 'Filled Mini Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ filledCart } />,
},
{
view: 'woocommerce/empty-mini-cart-contents-block',
label: __( 'Empty Mini Cart', 'woo-gutenberg-products-block' ),
icon: <Icon icon={ removeCart } />,
},
];
interface Props {
clientId: string;
}
const Edit = ( { clientId }: Props ): ReactElement => {
const Edit = ( { clientId, attributes }: Props ): ReactElement => {
const blockProps = useBlockProps( {
/**
* This is a workaround for the Site Editor to calculate the
@ -63,10 +43,7 @@ const Edit = ( { clientId }: Props ): ReactElement => {
[ 'woocommerce/empty-mini-cart-contents-block', {}, [] ],
] as TemplateArray;
const { currentView, component: ViewSwitcherComponent } = useViewSwitcher(
clientId,
views
);
const { currentView } = attributes;
useForcedLayout( {
clientId,
@ -130,7 +107,6 @@ const Edit = ( { clientId }: Props ): ReactElement => {
return (
<div { ...blockProps }>
<EditorProvider currentView={ currentView }>
<BlockControls>{ ViewSwitcherComponent }</BlockControls>
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={ defaultTemplate }

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { cart } from '@woocommerce/icons';
import { cart, filledCart, removeCart } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { registerBlockType } from '@wordpress/blocks';
import type { BlockConfiguration } from '@wordpress/blocks';
@ -46,7 +46,6 @@ const settings: BlockConfiguration = {
isPreview: {
type: 'boolean',
default: false,
save: false,
},
lock: {
type: 'object',
@ -55,6 +54,32 @@ const settings: BlockConfiguration = {
move: true,
},
},
currentView: {
type: 'string',
default: 'woocommerce/filled-mini-cart-contents-block',
source: 'readonly', // custom source to prevent saving to post content
},
editorViews: {
type: 'object',
default: [
{
view: 'woocommerce/filled-mini-cart-contents-block',
label: __(
'Filled Mini Cart',
'woo-gutenberg-products-block'
),
icon: <Icon icon={ filledCart } />,
},
{
view: 'woocommerce/empty-mini-cart-contents-block',
label: __(
'Empty Mini Cart',
'woo-gutenberg-products-block'
),
icon: <Icon icon={ removeCart } />,
},
],
},
},
example: {
attributes: {

View File

@ -5,7 +5,6 @@ export const blockAttributes = {
isPreview: {
type: 'boolean',
default: false,
save: false,
},
/**
* The product ID to display.

View File

@ -140,15 +140,6 @@
* shouldCreateAccount property.
*/
/**
* @typedef {Object} EditorDataContext
*
* @property {boolean} isEditor Indicates whether in the editor context.
* @property {number} currentPostId The post ID being edited.
* @property {Object} previewData Object containing preview data for the editor.
* @property {function(string):Object} getPreviewData Get data by name.
*/
/**
* @typedef {Object} AddToCartFormContext
*

View File

@ -71,7 +71,6 @@
"@types/react-dom": "18.0.9",
"@types/wordpress__block-editor": "6.0.6",
"@types/wordpress__blocks": "11.0.7",
"@types/wordpress__compose": "4.0.1",
"@types/wordpress__core-data": "^2.4.5",
"@types/wordpress__data": "^6.0.1",
"@types/wordpress__data-controls": "2.2.0",
@ -11338,51 +11337,6 @@
"re-resizable": "^6.4.0"
}
},
"node_modules/@types/wordpress__compose": {
"version": "4.0.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*",
"@types/react": "*",
"@wordpress/element": "^3.0.0"
}
},
"node_modules/@types/wordpress__compose/node_modules/@types/react": {
"version": "16.14.25",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/wordpress__compose/node_modules/@types/react-dom": {
"version": "16.9.15",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "^16"
}
},
"node_modules/@types/wordpress__compose/node_modules/@wordpress/element": {
"version": "3.2.0",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@wordpress/escape-html": "^2.2.0",
"lodash": "^4.17.21",
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@types/wordpress__core-data": {
"version": "2.4.5",
"dev": true,
@ -57902,46 +57856,6 @@
"re-resizable": "^6.4.0"
}
},
"@types/wordpress__compose": {
"version": "4.0.1",
"dev": true,
"requires": {
"@types/lodash": "*",
"@types/react": "*",
"@wordpress/element": "^3.0.0"
},
"dependencies": {
"@types/react": {
"version": "16.14.25",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/react-dom": {
"version": "16.9.15",
"dev": true,
"requires": {
"@types/react": "^16"
}
},
"@wordpress/element": {
"version": "3.2.0",
"dev": true,
"requires": {
"@babel/runtime": "^7.13.10",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@wordpress/escape-html": "^2.2.0",
"lodash": "^4.17.21",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}
}
},
"@types/wordpress__core-data": {
"version": "2.4.5",
"dev": true

View File

@ -23,6 +23,7 @@
"./assets/js/blocks/mini-cart/mini-cart-contents/inner-blocks/**/index.tsx",
"./assets/js/blocks/mini-cart/mini-cart-contents/inner-blocks/register-components.ts",
"./assets/js/blocks/cart-checkout-shared/sidebar-notices/index.tsx",
"./assets/js/blocks/cart-checkout-shared/view-switcher/index.tsx",
"./assets/js/blocks/filter-wrapper/register-components.ts",
"./assets/js/blocks/product-query/variations/**.tsx",
"./assets/js/blocks/product-query/index.tsx",
@ -122,7 +123,6 @@
"@types/react-dom": "18.0.9",
"@types/wordpress__block-editor": "6.0.6",
"@types/wordpress__blocks": "11.0.7",
"@types/wordpress__compose": "4.0.1",
"@types/wordpress__core-data": "^2.4.5",
"@types/wordpress__data": "^6.0.1",
"@types/wordpress__data-controls": "2.2.0",