Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Add Feedback Prompt in Cart & Checkout blocks sidebar #1356

Merged
merged 16 commits into from
Dec 16, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/js/base/context/product-layout-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const validationMap = {
};

/**
* ProductLayoutContext is an configuration object for layout options shared
* ProductLayoutContext is a configuration object for layout options shared
* among all components in a tree.
*
* @var {React.Context} ProductLayoutContext A react context object
Expand Down
2 changes: 1 addition & 1 deletion assets/js/blocks/cart-checkout/cart/edit.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { BlockControls } from '@wordpress/block-editor';
import { Toolbar } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import TextToolbarButton from '@woocommerce/block-components/text-toolbar-button';
import PropTypes from 'prop-types';

Expand Down
1 change: 1 addition & 0 deletions assets/js/hocs/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { default as withAttributes } from './with-attributes';
export { default as withCategories } from './with-categories';
export { default as withCategory } from './with-category';
export { default as withFeedbackPrompt } from './with-feedback-prompt';
export { default as withProduct } from './with-product';
export { default as withProductVariations } from './with-product-variations';
export { default as withSearchedProducts } from './with-searched-products';
Expand Down
45 changes: 45 additions & 0 deletions assets/js/hocs/with-feedback-prompt/feedback-prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import Gridicon from 'gridicons';

/**
* Internal dependencies
*/
import './style.scss';

/**
* Component to render a Feedback prompt in the sidebar.
*/
const FeedbackPrompt = ( { text } ) => {
return (
<div className="wc-block-feedback-prompt">
<Gridicon icon="comment" />
<h2 className="wc-block-feedback-prompt__title">
{ __( 'Feedback?', 'woo-gutenberg-products-block' ) }
</h2>
<p className="wc-block-feedback-prompt__text">{ text }</p>
<a
// @todo Update the link to a page to gather feedback.
href="https://wordpress.org/support/plugin/woo-gutenberg-products-block/reviews/"
className="wc-block-feedback-prompt__link"
rel="noreferrer noopener"
target="_blank"
>
{ __(
'Give us your feedback.',
'woo-gutenberg-products-block'
) }
<Gridicon icon="external" size={ 16 } />
</a>
</div>
);
};

FeedbackPrompt.propTypes = {
text: PropTypes.string,
};

export default FeedbackPrompt;
51 changes: 51 additions & 0 deletions assets/js/hocs/with-feedback-prompt/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Fragment } from '@wordpress/element';
import { InspectorControls } from '@wordpress/block-editor';
import { createHigherOrderComponent } from '@wordpress/compose';

/**
* Internal dependencies
*/
import FeedbackPrompt from './feedback-prompt.js';

const blocksFeedback = {
'woocommerce/cart': __(
'We are currently working on improving our checkout and providing merchants with tools and options to customize their checkout to their stores needs.',
'woo-gutenberg-products-block'
),
'woocommerce/checkout': __(
'We are currently working on improving our checkout and providing merchants with tools and options to customize their checkout to their stores needs.',
'woo-gutenberg-products-block'
),
};

/**
* Adds a feedback prompt to the editor sidebar.
*
* @param {WPComponent} BlockEdit Original component.
*
* @return {WPComponent} Wrapped component.
*/
const withFeedbackPrompt = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
const feedbackPromptText = blocksFeedback[ props.name ];
Copy link
Contributor

@nerrad nerrad Dec 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a neat approach and I ❤️ what you've done here with the HOC and how it's implemented using the filter. The only thing I'm wary about is having all the customization for feedback prompt in this one file (which is right now just content). It'd be more discoverable and easier to work with if individual blocks defined their specific elements. What about one of these options?

Option 1: specific named props for feedback prompt content and link.

In this option, edit components for blocks would be fed specific props that get picked up automatically by this HOC. They'd need to be namespaced appropriately.

  • wcFeedbackContent: For the custom content.
  • wcFeedbackUrl: For the url where people submit their feedback.

Option 2: use React.Context

This would be similar to option 2 but would utilize react context instead and the withFeedbackPrompt hoc would just read the values from the context. Edit blocks would then just explicitly wrap anything receiving feedback with the feedback prompt provider:

edit

<FeedbackProvider value={ feedBackConfig }>
    <EditComponent>
</FeedbackProvider>

withFeedbackPrompt:

const withFeedbackPrompt = createHigherOrderComponent( ( BlockEdit ) => {
	return ( props ) => {
        const { content, url } = useFeedbackContext();
        if ( content ) { 
            /** do stuff **/

On the surface it seems like the context would be a bit more work, but the advantage is that the EditComponent or anything else in its composition doesn't have to care or worry about passing through any of the props and I like the explicitness in using the provider which makes it clear that this is a component with a feedback element. I wonder if the Provider itself could take care of adding the editor.BlockEdit filter so everything is contained?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was playing with this idea but I'm not sure it's possible. The editor.BlockEdit is applied on an upper level than the block edit function is called, so wrapping the <Edit> block with a context provider, doesn't make that context available when calling the editor.BlockEdit filter.

I pushed a WIP for the checkout block here: 21f889b. Something similar would happen with props (option 1): props passed in the block edit function are not available in the editor.BlockEdit.

Please, let me know if I'm missing something!


To solve the issue you raised:

The only thing I'm wary about is having all the customization for feedback prompt in this one file (which is right now just content). It'd be more discoverable and easier to work with if individual blocks defined their specific elements.

If the options above don't work, I thought about some alternative solutions:

  • If we don't use the filter but rely only on the HOC like in 718d3f2, we would have access to props/context defined in the edit function.
  • We could alternatively create a blockFeedbackRegistry similar to the blocksRegistry we currently have in place. That would allow us to call a registerBlockFeedback() function right after registerBlockType() and have access to the blockFeedbackRegistry inside the editor.BlockEdit filter.

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya you're right the suggestions I have won't work because of how the filter implements the component outside the tree (illustrating one of the flaws of filters!).

I like the blockFeedbackRegistry idea, but I wonder how discoverable that api is, plus it'd be nice to avoid the filter because it feels fragile. Also the filter callback (the HOC) will get invoked for every block.

So I like the direction you are going with the hoc, but we could still use the context and have the hoc read from the context. That way we're not polluting the props unnecessarily for the block and can control where the feedback prompt appears in the inspector controls via the hoc:

withFeedbackPrompt:

/**
 * Adds a feedback prompt to the editor sidebar.
 *
 * @param {WPComponent} BlockEdit Original component.
 *
 * @return {WPComponent} Wrapped component.
 */
const withFeedbackPrompt = createHigherOrderComponent( ( BlockEdit ) => {
	return ( props ) => {
		const { content } = useFeedbackFormContext();
		if ( content ) {
			return (
				<Fragment>
					<BlockEdit { ...props } />
					<InspectorControls>
						<FeedbackPrompt text={ content } />
					</InspectorControls>
				</Fragment>
			);
		}

		return <BlockEdit { ...props } />;
	};
}, 'withFeedbackPrompt' );

Then wrap the Edit component for the block in withFeedbackPrompt.

Then in the block registration you could have something like this:

edit: ( props ) => {
		const feedbackContent = __(
			'We are currently working on improving our checkout and providing merchants with tools and options to customize their checkout to their stores needs.',
			'woo-gutenberg-products-block'
		);
		return (
			<FeedbackFormProvider content={ feedbackContent }>
				<BlockEdit { ...props } />
			</FeedbackFormProvider>
		);
	},

Alternatively, you could just curry the withFeedbackPrompt hoc so it receives the content and url in the curried function returning the HOC itself. So your implementation in edit would be something like:

export default withFeedbackPrompt( __('content for feedback prompt'), 'https://urltofeedback.com' )( Edit );

I'd be happy with either alternative but I think the currying is probably the most straightforward (and less code). Context feels like it may be overkill?


if ( feedbackPromptText && props.isSelected ) {
return (
<Fragment>
<BlockEdit { ...props } />
<InspectorControls>
<FeedbackPrompt text={ feedbackPromptText } />
</InspectorControls>
</Fragment>
);
}

return <BlockEdit { ...props } />;
};
}, 'withFeedbackPrompt' );

export default withFeedbackPrompt;
19 changes: 19 additions & 0 deletions assets/js/hocs/with-feedback-prompt/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.wc-block-feedback-prompt {
background-color: #f7f7f7;
border-top: 1px solid #e2e4e7;
margin: $gap -16px -16px;
padding: $gap-large;
text-align: center;

.wc-block-feedback-prompt__title {
margin: 0 0 $gap-small;
}

.wc-block-feedback-prompt__link {
color: inherit;

> .gridicon {
vertical-align: text-bottom;
}
}
}
8 changes: 8 additions & 0 deletions assets/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*/
import { getCategories, setCategories } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { addFilter } from '@wordpress/hooks';
import { withFeedbackPrompt } from '@woocommerce/block-hocs';

/**
* Internal dependencies
Expand All @@ -20,3 +22,9 @@ setCategories( [
icon: <IconWoo />,
},
] );

addFilter(
'editor.BlockEdit',
'woocommerce/blocks/with-feedback-prompt',
withFeedbackPrompt
);