Merge pull request #32538 from woocommerce/feature/32158_complete_task_list_card_with_feedback
Feature/32158 complete task list card with feedback
This commit is contained in:
commit
e822a4a7f4
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add new simple customer feedback component for inline CES feedback. #32538
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Add TypeScript type support as part of the build process. #32538
|
|
@ -18,6 +18,7 @@
|
||||||
},
|
},
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"module": "build-module/index.js",
|
"module": "build-module/index.js",
|
||||||
|
"types": "build-types",
|
||||||
"react-native": "src/index",
|
"react-native": "src/index",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@woocommerce/experimental": "workspace:*",
|
"@woocommerce/experimental": "workspace:*",
|
||||||
|
|
|
@ -4,13 +4,12 @@
|
||||||
import { createElement, useState, useEffect } from '@wordpress/element';
|
import { createElement, useState, useEffect } from '@wordpress/element';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { compose } from '@wordpress/compose';
|
import { useDispatch } from '@wordpress/data';
|
||||||
import { withDispatch } from '@wordpress/data';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CustomerFeedbackModal from './customer-feedback-modal';
|
import { CustomerFeedbackModal } from './customer-feedback-modal';
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
|
@ -23,16 +22,14 @@ const noop = () => {};
|
||||||
* @param {Object} props Component props.
|
* @param {Object} props Component props.
|
||||||
* @param {Function} props.recordScoreCallback Function to call when the score should be recorded.
|
* @param {Function} props.recordScoreCallback Function to call when the score should be recorded.
|
||||||
* @param {string} props.label The label displayed in the modal.
|
* @param {string} props.label The label displayed in the modal.
|
||||||
* @param {Function} props.createNotice Create a notice (snackbar).
|
|
||||||
* @param {Function} props.onNoticeShownCallback Function to call when the notice is shown.
|
* @param {Function} props.onNoticeShownCallback Function to call when the notice is shown.
|
||||||
* @param {Function} props.onNoticeDismissedCallback Function to call when the notice is dismissed.
|
* @param {Function} props.onNoticeDismissedCallback Function to call when the notice is dismissed.
|
||||||
* @param {Function} props.onModalShownCallback Function to call when the modal is shown.
|
* @param {Function} props.onModalShownCallback Function to call when the modal is shown.
|
||||||
* @param {Object} props.icon Icon (React component) to be shown on the notice.
|
* @param {Object} props.icon Icon (React component) to be shown on the notice.
|
||||||
*/
|
*/
|
||||||
export function CustomerEffortScore( {
|
function CustomerEffortScore( {
|
||||||
recordScoreCallback,
|
recordScoreCallback,
|
||||||
label,
|
label,
|
||||||
createNotice,
|
|
||||||
onNoticeShownCallback = noop,
|
onNoticeShownCallback = noop,
|
||||||
onNoticeDismissedCallback = noop,
|
onNoticeDismissedCallback = noop,
|
||||||
onModalShownCallback = noop,
|
onModalShownCallback = noop,
|
||||||
|
@ -40,6 +37,7 @@ export function CustomerEffortScore( {
|
||||||
} ) {
|
} ) {
|
||||||
const [ shouldCreateNotice, setShouldCreateNotice ] = useState( true );
|
const [ shouldCreateNotice, setShouldCreateNotice ] = useState( true );
|
||||||
const [ visible, setVisible ] = useState( false );
|
const [ visible, setVisible ] = useState( false );
|
||||||
|
const { createNotice } = useDispatch( 'core/notices2' );
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( ! shouldCreateNotice ) {
|
if ( ! shouldCreateNotice ) {
|
||||||
|
@ -91,10 +89,6 @@ CustomerEffortScore.propTypes = {
|
||||||
* The label displayed in the modal.
|
* The label displayed in the modal.
|
||||||
*/
|
*/
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
/**
|
|
||||||
* Create a notice (snackbar).
|
|
||||||
*/
|
|
||||||
createNotice: PropTypes.func.isRequired,
|
|
||||||
/**
|
/**
|
||||||
* The function to call when the notice is shown.
|
* The function to call when the notice is shown.
|
||||||
*/
|
*/
|
||||||
|
@ -113,12 +107,4 @@ CustomerEffortScore.propTypes = {
|
||||||
icon: PropTypes.element,
|
icon: PropTypes.element,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default compose(
|
export { CustomerEffortScore };
|
||||||
withDispatch( ( dispatch ) => {
|
|
||||||
const { createNotice } = dispatch( 'core/notices2' );
|
|
||||||
|
|
||||||
return {
|
|
||||||
createNotice,
|
|
||||||
};
|
|
||||||
} )
|
|
||||||
)( CustomerEffortScore );
|
|
|
@ -26,13 +26,19 @@ import { __ } from '@wordpress/i18n';
|
||||||
* @param {Object} props Component props.
|
* @param {Object} props Component props.
|
||||||
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
|
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
|
||||||
* @param {string} props.label Question to ask the customer.
|
* @param {string} props.label Question to ask the customer.
|
||||||
|
* @param {string} props.defaultScore Default score.
|
||||||
|
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
|
||||||
*/
|
*/
|
||||||
function CustomerFeedbackModal( {
|
function CustomerFeedbackModal( {
|
||||||
recordScoreCallback,
|
recordScoreCallback,
|
||||||
label,
|
label,
|
||||||
|
defaultScore = NaN,
|
||||||
|
onCloseModal,
|
||||||
}: {
|
}: {
|
||||||
recordScoreCallback: ( score: number, comments: string ) => void;
|
recordScoreCallback: ( score: number, comments: string ) => void;
|
||||||
label: string;
|
label: string;
|
||||||
|
defaultScore?: number;
|
||||||
|
onCloseModal?: () => void;
|
||||||
} ): JSX.Element | null {
|
} ): JSX.Element | null {
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
|
@ -57,12 +63,17 @@ function CustomerFeedbackModal( {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const [ score, setScore ] = useState( NaN );
|
const [ score, setScore ] = useState( defaultScore || NaN );
|
||||||
const [ comments, setComments ] = useState( '' );
|
const [ comments, setComments ] = useState( '' );
|
||||||
const [ showNoScoreMessage, setShowNoScoreMessage ] = useState( false );
|
const [ showNoScoreMessage, setShowNoScoreMessage ] = useState( false );
|
||||||
const [ isOpen, setOpen ] = useState( true );
|
const [ isOpen, setOpen ] = useState( true );
|
||||||
|
|
||||||
const closeModal = () => setOpen( false );
|
const closeModal = () => {
|
||||||
|
setOpen( false );
|
||||||
|
if ( onCloseModal ) {
|
||||||
|
onCloseModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onRadioControlChange = ( value: string ) => {
|
const onRadioControlChange = ( value: string ) => {
|
||||||
const valueAsInt = parseInt( value, 10 );
|
const valueAsInt = parseInt( value, 10 );
|
||||||
|
@ -111,7 +122,7 @@ function CustomerFeedbackModal( {
|
||||||
{ ( score === 1 || score === 2 ) && (
|
{ ( score === 1 || score === 2 ) && (
|
||||||
<div className="woocommerce-customer-effort-score__comments">
|
<div className="woocommerce-customer-effort-score__comments">
|
||||||
<TextareaControl
|
<TextareaControl
|
||||||
label={ __( 'Comments (Optional)', 'woocommerce' ) }
|
label={ __( 'Comments (optional)', 'woocommerce' ) }
|
||||||
help={ __(
|
help={ __(
|
||||||
'Your feedback will go to the WooCommerce development team',
|
'Your feedback will go to the WooCommerce development team',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
|
@ -152,6 +163,8 @@ function CustomerFeedbackModal( {
|
||||||
CustomerFeedbackModal.propTypes = {
|
CustomerFeedbackModal.propTypes = {
|
||||||
recordScoreCallback: PropTypes.func.isRequired,
|
recordScoreCallback: PropTypes.func.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
|
defaultScore: PropTypes.number,
|
||||||
|
onCloseModal: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomerFeedbackModal;
|
export { CustomerFeedbackModal };
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { createElement } from '@wordpress/element';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CustomerFeedbackModal from '../index';
|
import { CustomerFeedbackModal } from '../index';
|
||||||
|
|
||||||
const mockRecordScoreCallback = jest.fn();
|
const mockRecordScoreCallback = jest.fn();
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ describe( 'CustomerFeedbackModal', () => {
|
||||||
await screen.findByRole( 'dialog' );
|
await screen.findByRole( 'dialog' );
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.queryByLabelText( 'Comments (Optional)' )
|
screen.queryByLabelText( 'Comments (optional)' )
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ describe( 'CustomerFeedbackModal', () => {
|
||||||
fireEvent.click( screen.getByLabelText( labelText ) );
|
fireEvent.click( screen.getByLabelText( labelText ) );
|
||||||
|
|
||||||
// Wait for comments field to show.
|
// Wait for comments field to show.
|
||||||
await screen.findByLabelText( 'Comments (Optional)' );
|
await screen.findByLabelText( 'Comments (optional)' );
|
||||||
|
|
||||||
// Select neutral score.
|
// Select neutral score.
|
||||||
fireEvent.click( screen.getByLabelText( 'Neutral' ) );
|
fireEvent.click( screen.getByLabelText( 'Neutral' ) );
|
||||||
|
@ -88,7 +88,7 @@ describe( 'CustomerFeedbackModal', () => {
|
||||||
// Wait for comments field to hide.
|
// Wait for comments field to hide.
|
||||||
await waitFor( () => {
|
await waitFor( () => {
|
||||||
expect(
|
expect(
|
||||||
screen.queryByLabelText( 'Comments (Optional)' )
|
screen.queryByLabelText( 'Comments (optional)' )
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
.customer-feedback-simple__container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.components-button {
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-feedback-simple__selection {
|
||||||
|
margin-left: $gap-small;
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Tooltip } from '@wordpress/components';
|
||||||
|
import { Text } from '@woocommerce/experimental';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
type CustomerFeedbackSimpleProps = {
|
||||||
|
onSelect: ( score: number ) => void;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a modal requesting customer feedback.
|
||||||
|
*
|
||||||
|
* A label is displayed in the modal asking the customer to score the
|
||||||
|
* difficulty completing a task. A group of radio buttons, styled with
|
||||||
|
* emoji facial expressions, are used to provide a score between 1 and 5.
|
||||||
|
*
|
||||||
|
* A low score triggers a comments field to appear.
|
||||||
|
*
|
||||||
|
* Upon completion, the score and comments is sent to a callback function.
|
||||||
|
*
|
||||||
|
* @param {Object} props Component props.
|
||||||
|
* @param {Function} props.onSelect Function to call when the results are sent.
|
||||||
|
* @param {string} props.label Question to ask the customer.
|
||||||
|
*/
|
||||||
|
const CustomerFeedbackSimple: React.FC< CustomerFeedbackSimpleProps > = ( {
|
||||||
|
onSelect,
|
||||||
|
label,
|
||||||
|
} ) => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
tooltip: __( 'Very difficult', 'woocommerce' ),
|
||||||
|
value: 1,
|
||||||
|
emoji: '😞',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: __( 'Difficult', 'woocommerce' ),
|
||||||
|
value: 2,
|
||||||
|
emoji: '🙁',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: __( 'Neutral', 'woocommerce' ),
|
||||||
|
value: 3,
|
||||||
|
emoji: '😑',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: __( 'Good', 'woocommerce' ),
|
||||||
|
value: 4,
|
||||||
|
emoji: '🙂',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: __( 'Very good', 'woocommerce' ),
|
||||||
|
value: 5,
|
||||||
|
emoji: '😍',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="customer-feedback-simple__container">
|
||||||
|
<Text variant="subtitle.small" as="p" size="13" lineHeight="16px">
|
||||||
|
{ label }
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="customer-feedback-simple__selection">
|
||||||
|
{ options.map( ( option ) => (
|
||||||
|
<Tooltip
|
||||||
|
text={ option.tooltip }
|
||||||
|
key={ option.value }
|
||||||
|
position="top center"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={ () => {
|
||||||
|
onSelect( option.value );
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ option.emoji }
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomerFeedbackSimple.propTypes = {
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CustomerFeedbackSimple };
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './customer-feedback-simple';
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { CustomerFeedbackSimple } from '../index';
|
||||||
|
|
||||||
|
const mockOnSelectCallback = jest.fn();
|
||||||
|
|
||||||
|
describe( 'CustomerFeedbackSimple', () => {
|
||||||
|
it( 'should trigger recordScoreCallback when item is selected', () => {
|
||||||
|
render( <CustomerFeedbackSimple onSelect={ mockOnSelectCallback } /> );
|
||||||
|
|
||||||
|
// Select the option.
|
||||||
|
fireEvent.click( screen.getAllByText( '🙂' )[ 0 ] );
|
||||||
|
|
||||||
|
expect( mockOnSelectCallback ).toHaveBeenCalledWith( 4 );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './customer-effort-score';
|
||||||
|
export * from './customer-feedback-simple';
|
||||||
|
export * from './customer-feedback-modal';
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'customer-feedback-simple/customer-feedback-simple.scss';
|
||||||
|
|
||||||
.woocommerce-customer-effort-score__selection {
|
.woocommerce-customer-effort-score__selection {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
input[value='3'] + label::before {
|
input[value='3'] + label::before {
|
||||||
content: '😐';
|
content: '😑';
|
||||||
}
|
}
|
||||||
|
|
||||||
input[value='4'] + label::before {
|
input[value='4'] + label::before {
|
||||||
|
@ -76,7 +78,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
input[value='5'] + label::before {
|
input[value='5'] + label::before {
|
||||||
content: '😁';
|
content: '😍';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,22 +3,37 @@
|
||||||
*/
|
*/
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { createElement } from '@wordpress/element';
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { CustomerEffortScore } from '../index';
|
import { CustomerEffortScore } from '../customer-effort-score';
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
|
jest.mock( '@wordpress/data', () => {
|
||||||
|
const originalModule = jest.requireActual( '@wordpress/data' );
|
||||||
|
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
useDispatch: jest.fn().mockReturnValue( {
|
||||||
|
createNotice: jest.fn(),
|
||||||
|
} ),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
describe( 'CustomerEffortScore', () => {
|
describe( 'CustomerEffortScore', () => {
|
||||||
it( 'should call createNotice with appropriate parameters', async () => {
|
it( 'should call createNotice with appropriate parameters', async () => {
|
||||||
const mockCreateNotice = jest.fn();
|
const mockCreateNotice = jest.fn();
|
||||||
|
useDispatch.mockReturnValue( {
|
||||||
|
createNotice: mockCreateNotice,
|
||||||
|
} );
|
||||||
const icon = <span>icon</span>;
|
const icon = <span>icon</span>;
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CustomerEffortScore
|
<CustomerEffortScore
|
||||||
createNotice={ mockCreateNotice }
|
|
||||||
recordScoreCallback={ noop }
|
recordScoreCallback={ noop }
|
||||||
label={ 'label' }
|
label={ 'label' }
|
||||||
onNoticeDismissedCallback={ noop }
|
onNoticeDismissedCallback={ noop }
|
||||||
|
@ -41,6 +56,9 @@ describe( 'CustomerEffortScore', () => {
|
||||||
|
|
||||||
it( 'should not call createNotice on rerender', async () => {
|
it( 'should not call createNotice on rerender', async () => {
|
||||||
const mockCreateNotice = jest.fn();
|
const mockCreateNotice = jest.fn();
|
||||||
|
useDispatch.mockReturnValue( {
|
||||||
|
createNotice: mockCreateNotice,
|
||||||
|
} );
|
||||||
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<CustomerEffortScore
|
<CustomerEffortScore
|
||||||
|
@ -53,7 +71,6 @@ describe( 'CustomerEffortScore', () => {
|
||||||
// Simulate rerender by changing label prop.
|
// Simulate rerender by changing label prop.
|
||||||
rerender(
|
rerender(
|
||||||
<CustomerEffortScore
|
<CustomerEffortScore
|
||||||
createNotice={ mockCreateNotice }
|
|
||||||
recordScoreCallback={ noop }
|
recordScoreCallback={ noop }
|
||||||
label={ 'label2' }
|
label={ 'label2' }
|
||||||
/>
|
/>
|
||||||
|
@ -65,7 +82,6 @@ describe( 'CustomerEffortScore', () => {
|
||||||
it( 'should not show dialog if no action is taken', async () => {
|
it( 'should not show dialog if no action is taken', async () => {
|
||||||
render(
|
render(
|
||||||
<CustomerEffortScore
|
<CustomerEffortScore
|
||||||
createNotice={ noop }
|
|
||||||
recordScoreCallback={ noop }
|
recordScoreCallback={ noop }
|
||||||
label={ 'label' }
|
label={ 'label' }
|
||||||
/>
|
/>
|
||||||
|
@ -91,10 +107,12 @@ describe( 'CustomerEffortScore', () => {
|
||||||
// Modal shown callback should also be called.
|
// Modal shown callback should also be called.
|
||||||
expect( mockOnModalShownCallback ).toHaveBeenCalled();
|
expect( mockOnModalShownCallback ).toHaveBeenCalled();
|
||||||
};
|
};
|
||||||
|
useDispatch.mockReturnValue( {
|
||||||
|
createNotice,
|
||||||
|
} );
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CustomerEffortScore
|
<CustomerEffortScore
|
||||||
createNotice={ createNotice }
|
|
||||||
recordScoreCallback={ noop }
|
recordScoreCallback={ noop }
|
||||||
label={ 'label' }
|
label={ 'label' }
|
||||||
onModalShownCallback={ mockOnModalShownCallback }
|
onModalShownCallback={ mockOnModalShownCallback }
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
"extends": "../tsconfig",
|
"extends": "../tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "build-module"
|
"outDir": "build-module",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"declarationDir": "./build-types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Added TypeScript options selectors and action in onboarding store for keeping the completed task list. #32158
|
|
@ -105,6 +105,7 @@ import { WPDataSelectors } from './types';
|
||||||
import { PaymentSelectors } from './payment-gateways/selectors';
|
import { PaymentSelectors } from './payment-gateways/selectors';
|
||||||
import { PluginSelectors } from './plugins/selectors';
|
import { PluginSelectors } from './plugins/selectors';
|
||||||
import { OnboardingSelectors } from './onboarding/selectors';
|
import { OnboardingSelectors } from './onboarding/selectors';
|
||||||
|
import { OptionsSelectors } from './options/types';
|
||||||
|
|
||||||
// As we add types to all the package selectors we can fill out these unknown types with real ones. See one
|
// As we add types to all the package selectors we can fill out these unknown types with real ones. See one
|
||||||
// of the already typed selectors for an example of how you can do this.
|
// of the already typed selectors for an example of how you can do this.
|
||||||
|
@ -121,7 +122,7 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME
|
||||||
: T extends typeof USER_STORE_NAME
|
: T extends typeof USER_STORE_NAME
|
||||||
? WPDataSelectors
|
? WPDataSelectors
|
||||||
: T extends typeof OPTIONS_STORE_NAME
|
: T extends typeof OPTIONS_STORE_NAME
|
||||||
? WPDataSelectors
|
? OptionsSelectors
|
||||||
: T extends typeof NAVIGATION_STORE_NAME
|
: T extends typeof NAVIGATION_STORE_NAME
|
||||||
? WPDataSelectors
|
? WPDataSelectors
|
||||||
: T extends typeof NOTES_STORE_NAME
|
: T extends typeof NOTES_STORE_NAME
|
||||||
|
|
|
@ -34,6 +34,8 @@ const TYPES = {
|
||||||
ACTION_TASK_REQUEST: 'ACTION_TASK_REQUEST',
|
ACTION_TASK_REQUEST: 'ACTION_TASK_REQUEST',
|
||||||
ACTION_TASK_SUCCESS: 'ACTION_TASK_SUCCESS',
|
ACTION_TASK_SUCCESS: 'ACTION_TASK_SUCCESS',
|
||||||
VISITED_TASK: 'VISITED_TASK',
|
VISITED_TASK: 'VISITED_TASK',
|
||||||
|
KEEP_COMPLETED_TASKS_REQUEST: 'KEEP_COMPLETED_TASKS_REQUEST',
|
||||||
|
KEEP_COMPLETED_TASKS_SUCCESS: 'KEEP_COMPLETED_TASKS_SUCCESS',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TYPES;
|
export default TYPES;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { apiFetch } from '@wordpress/data-controls';
|
import { apiFetch } from '@wordpress/data-controls';
|
||||||
|
import { controls } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -9,6 +10,7 @@ import { apiFetch } from '@wordpress/data-controls';
|
||||||
import TYPES from './action-types';
|
import TYPES from './action-types';
|
||||||
import { WC_ADMIN_NAMESPACE } from '../constants';
|
import { WC_ADMIN_NAMESPACE } from '../constants';
|
||||||
import { DeprecatedTasks } from './deprecated-tasks';
|
import { DeprecatedTasks } from './deprecated-tasks';
|
||||||
|
import { STORE_NAME as OPTIONS_STORE_NAME } from '../options/constants';
|
||||||
|
|
||||||
export function getFreeExtensionsError( error ) {
|
export function getFreeExtensionsError( error ) {
|
||||||
return {
|
return {
|
||||||
|
@ -203,6 +205,14 @@ export function optimisticallyCompleteTaskRequest( taskId ) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function keepCompletedTaskListSuccess( taskListId, keepCompletedList ) {
|
||||||
|
return {
|
||||||
|
type: TYPES.KEEP_COMPLETED_TASKS_SUCCESS,
|
||||||
|
taskListId,
|
||||||
|
keepCompletedTaskList: keepCompletedList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function visitedTask( taskId ) {
|
export function visitedTask( taskId ) {
|
||||||
return {
|
return {
|
||||||
type: TYPES.VISITED_TASK,
|
type: TYPES.VISITED_TASK,
|
||||||
|
@ -260,6 +270,20 @@ export function getProductTypesError( error ) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* keepCompletedTaskList( taskListId ) {
|
||||||
|
const updateOptionsParams = {
|
||||||
|
woocommerce_task_list_keep_completed: 'yes',
|
||||||
|
};
|
||||||
|
const response = yield controls.dispatch(
|
||||||
|
OPTIONS_STORE_NAME,
|
||||||
|
'updateOptions',
|
||||||
|
updateOptionsParams
|
||||||
|
);
|
||||||
|
if ( response && response.success ) {
|
||||||
|
yield keepCompletedTaskListSuccess( taskListId, 'yes' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function* updateProfileItems( items ) {
|
export function* updateProfileItems( items ) {
|
||||||
yield setIsRequesting( 'updateProfileItems', true );
|
yield setIsRequesting( 'updateProfileItems', true );
|
||||||
yield setError( 'updateProfileItems', null );
|
yield setError( 'updateProfileItems', null );
|
||||||
|
|
|
@ -72,6 +72,7 @@ const onboarding = (
|
||||||
taskListId,
|
taskListId,
|
||||||
taskList,
|
taskList,
|
||||||
taskLists,
|
taskLists,
|
||||||
|
keepCompletedTaskList,
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
switch ( type ) {
|
switch ( type ) {
|
||||||
|
@ -372,6 +373,17 @@ const onboarding = (
|
||||||
[ taskListId ]: taskList,
|
[ taskListId ]: taskList,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
case TYPES.KEEP_COMPLETED_TASKS_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
taskLists: {
|
||||||
|
...state.taskLists,
|
||||||
|
[ taskListId ]: {
|
||||||
|
...state.taskLists[ taskListId ],
|
||||||
|
keepCompletedTaskList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
case TYPES.OPTIMISTICALLY_COMPLETE_TASK_REQUEST:
|
case TYPES.OPTIMISTICALLY_COMPLETE_TASK_REQUEST:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -51,6 +51,7 @@ export type TaskListType = {
|
||||||
eventPrefix: string;
|
eventPrefix: string;
|
||||||
displayProgressHeader: boolean;
|
displayProgressHeader: boolean;
|
||||||
keepCompletedTaskList: 'yes' | 'no';
|
keepCompletedTaskList: 'yes' | 'no';
|
||||||
|
showCESFeedback?: boolean;
|
||||||
sections?: TaskListSection[];
|
sections?: TaskListSection[];
|
||||||
isToggleable?: boolean;
|
isToggleable?: boolean;
|
||||||
isCollapsible?: boolean;
|
isCollapsible?: boolean;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { WPDataSelector, WPDataSelectors } from '../types';
|
||||||
|
import {
|
||||||
|
getOptionsRequestingError,
|
||||||
|
isOptionsUpdating,
|
||||||
|
getOptionsUpdatingError,
|
||||||
|
} from './selectors';
|
||||||
|
|
||||||
|
export type OptionsSelectors = {
|
||||||
|
getOption: < T = string >( option: string ) => T;
|
||||||
|
// getOption: getOption;
|
||||||
|
getOptionsRequestingError: WPDataSelector<
|
||||||
|
typeof getOptionsRequestingError
|
||||||
|
>;
|
||||||
|
isOptionsUpdating: WPDataSelector< typeof isOptionsUpdating >;
|
||||||
|
getOptionsUpdatingError: WPDataSelector< typeof getOptionsUpdatingError >;
|
||||||
|
} & WPDataSelectors;
|
|
@ -4,7 +4,7 @@
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import CustomerEffortScore from '@woocommerce/customer-effort-score';
|
import { CustomerEffortScore } from '@woocommerce/customer-effort-score';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import { withSelect, withDispatch } from '@wordpress/data';
|
import { withSelect, withDispatch } from '@wordpress/data';
|
||||||
import { OPTIONS_STORE_NAME, WEEK } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME, WEEK } from '@woocommerce/data';
|
||||||
|
@ -117,7 +117,7 @@ function CustomerEffortScoreTracks( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomerEffortScore
|
<CustomerEffortScore
|
||||||
recordScoreCallback={ recordScore }
|
onSelect={ recordScore }
|
||||||
label={ label }
|
label={ label }
|
||||||
onNoticeShownCallback={ onNoticeShown }
|
onNoticeShownCallback={ onNoticeShown }
|
||||||
onNoticeDismissedCallback={ onNoticeDismissed }
|
onNoticeDismissedCallback={ onNoticeDismissed }
|
||||||
|
|
|
@ -26,6 +26,10 @@ export type TaskListProps = TaskListType & {
|
||||||
query: {
|
query: {
|
||||||
task?: string;
|
task?: string;
|
||||||
};
|
};
|
||||||
|
eventName?: string;
|
||||||
|
twoColumns?: boolean;
|
||||||
|
keepCompletedTaskList?: 'yes' | 'no';
|
||||||
|
cesHeader?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TaskList: React.FC< TaskListProps > = ( {
|
export const TaskList: React.FC< TaskListProps > = ( {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
OPTIONS_STORE_NAME,
|
OPTIONS_STORE_NAME,
|
||||||
TaskListType,
|
TaskListType,
|
||||||
TaskType,
|
TaskType,
|
||||||
|
WCDataSelector,
|
||||||
} from '@woocommerce/data';
|
} from '@woocommerce/data';
|
||||||
import { useExperiment } from '@woocommerce/explat';
|
import { useExperiment } from '@woocommerce/explat';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
@ -66,14 +67,16 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => {
|
||||||
'woocommerce_tasklist_progression'
|
'woocommerce_tasklist_progression'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isResolving, taskLists } = useSelect( ( select ) => {
|
const { isResolving, taskLists } = useSelect(
|
||||||
|
( select: WCDataSelector ) => {
|
||||||
return {
|
return {
|
||||||
isResolving: ! select(
|
isResolving: ! select(
|
||||||
ONBOARDING_STORE_NAME
|
ONBOARDING_STORE_NAME
|
||||||
).hasFinishedResolution( 'getTaskLists' ),
|
).hasFinishedResolution( 'getTaskLists' ),
|
||||||
taskLists: select( ONBOARDING_STORE_NAME ).getTaskLists(),
|
taskLists: select( ONBOARDING_STORE_NAME ).getTaskLists(),
|
||||||
};
|
};
|
||||||
} );
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const getCurrentTask = () => {
|
const getCurrentTask = () => {
|
||||||
if ( ! task ) {
|
if ( ! task ) {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<svg width="453" height="73" viewBox="0 0 453 73" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="3.88561" height="12.2119" rx="1.94281" transform="matrix(-0.825943 0.563754 0.563755 0.825942 404.047 48.594)" fill="#64CA43"/>
|
||||||
|
<rect width="3.05264" height="9.59401" rx="1.52632" transform="matrix(-0.672177 0.740391 0.740391 0.672176 417.97 13.4998)" fill="#FF2D55"/>
|
||||||
|
<rect width="3.64276" height="11.4487" rx="1.82138" transform="matrix(-0.6382 -0.769871 -0.769872 0.638198 391.795 24.9504)" fill="#117AC9"/>
|
||||||
|
<rect width="3.88561" height="12.2119" rx="1.9428" transform="matrix(-0.404372 -0.914595 -0.914595 0.404371 170.94 14.6914)" fill="#FF8085"/>
|
||||||
|
<rect width="5.34271" height="16.7914" rx="2.67136" transform="matrix(0.39264 0.919692 0.919692 -0.392642 328.119 50.6055)" fill="#FF8085"/>
|
||||||
|
<circle r="3.43422" transform="matrix(-0.949193 -0.314694 -0.314694 0.949193 433.694 60.544)" fill="#F0B849"/>
|
||||||
|
<ellipse rx="2.28948" ry="2.28948" transform="matrix(-0.949193 -0.314695 -0.314693 0.949194 449.993 44.0008)" fill="#BF5AF2"/>
|
||||||
|
<ellipse rx="1.52632" ry="1.52632" transform="matrix(-0.949194 -0.314692 -0.314695 0.949193 373.339 63.31)" fill="#BF5AF2"/>
|
||||||
|
<ellipse rx="2.28948" ry="2.28948" transform="matrix(-0.949194 -0.314692 -0.314695 0.949193 160.713 54.097)" fill="#09B585"/>
|
||||||
|
<rect x="314.273" y="17.2192" width="5.34271" height="16.7914" rx="2.67136" transform="rotate(-51.7958 314.273 17.2192)" fill="#984A9C"/>
|
||||||
|
<rect width="3.88561" height="12.2119" rx="1.9428" transform="matrix(0.618465 -0.785812 0.78581 0.618467 27.061 34.741)" fill="#64CA43"/>
|
||||||
|
<rect width="3.64276" height="11.4487" rx="1.82138" transform="matrix(-0.988881 -0.148711 0.148714 -0.98888 267.602 27.863)" fill="#E7C037"/>
|
||||||
|
<rect width="3.00682" height="9.45" rx="1.50341" transform="matrix(0.226971 0.973902 -0.973902 0.226968 212.204 51)" fill="#E7C037"/>
|
||||||
|
<rect width="3.88561" height="12.2119" rx="1.9428" transform="matrix(0.78581 0.618468 -0.618465 0.785812 269.396 56.8789)" fill="#3361CC"/>
|
||||||
|
<circle cx="90.527" cy="45.6926" r="3.43422" transform="rotate(-1.79578 90.527 45.6926)" fill="#F0B849"/>
|
||||||
|
<circle cx="59.8596" cy="27.1158" r="2.28948" transform="rotate(-1.79576 59.8596 27.1158)" fill="#BF5AF2"/>
|
||||||
|
<circle cx="307.109" cy="60.7663" r="1.52632" transform="rotate(-1.79574 307.109 60.7663)" fill="#F0C930"/>
|
||||||
|
<circle cx="357.311" cy="28.5444" r="1.52632" transform="rotate(-1.79574 357.311 28.5444)" fill="#F0C930"/>
|
||||||
|
<ellipse cx="237.248" cy="47.3674" rx="1.52632" ry="1.52632" transform="rotate(-1.79578 237.248 47.3674)" fill="#3361CC"/>
|
||||||
|
<circle cx="290.869" cy="39.9329" r="1.9079" transform="rotate(-1.79577 290.869 39.9329)" fill="#37E688"/>
|
||||||
|
<rect width="3.88561" height="12.2119" rx="1.9428" transform="matrix(0.336735 -0.941599 0.941599 0.336737 108.684 60.751)" fill="#64CA43"/>
|
||||||
|
<rect x="131.252" y="25.1282" width="3.88561" height="12.2119" rx="1.9428" transform="rotate(5.81869 131.252 25.1282)" fill="#3361CC"/>
|
||||||
|
<ellipse rx="3.43422" ry="3.43422" transform="matrix(0.827262 -0.561816 0.561811 0.827266 21.4569 67.7751)" fill="#F0B849"/>
|
||||||
|
<circle cx="195.819" cy="33.1654" r="2.28948" transform="rotate(-34.1813 195.819 33.1654)" fill="#BF5AF2"/>
|
||||||
|
<circle r="1.52632" transform="matrix(0.827266 -0.56181 0.561818 0.827261 64.254 65.9745)" fill="#3361CC"/>
|
||||||
|
<ellipse rx="1.9079" ry="1.9079" transform="matrix(0.827265 -0.561812 0.561815 0.827263 2.58724 48.3031)" fill="#37E688"/>
|
||||||
|
<ellipse rx="1.9079" ry="1.9079" transform="matrix(0.827265 -0.561812 0.561815 0.827263 27.9769 15.6493)" fill="#F0C930"/>
|
||||||
|
<ellipse cx="231.367" cy="21.336" rx="2.28948" ry="2.28948" transform="rotate(-34.1813 231.367 21.336)" fill="#09B585"/>
|
||||||
|
<ellipse rx="2.28948" ry="2.28948" transform="matrix(0.827267 -0.561809 0.561819 0.82726 100.164 15.4271)" fill="#FF3B30"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,27 @@
|
||||||
|
$ces-feedback-height: 64px;
|
||||||
|
|
||||||
|
.wooocommerce-task-card__header .wooocommerce-task-card__header-subtitle {
|
||||||
|
color: $gray-700;
|
||||||
|
margin-bottom: $gap-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wooocommerce-task-card__finished-header-image {
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-feedback-simple__container {
|
||||||
|
height: $ces-feedback-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-task-card__header-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wooocommerce-task-card__header-ces-feedback {
|
||||||
|
height: $ces-feedback-height;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -0,0 +1,249 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
|
import { EllipsisMenu } from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { OPTIONS_STORE_NAME, WCDataSelector, WEEK } from '@woocommerce/data';
|
||||||
|
import { Button, Card, CardHeader } from '@wordpress/components';
|
||||||
|
import { Text } from '@woocommerce/experimental';
|
||||||
|
import {
|
||||||
|
CustomerFeedbackModal,
|
||||||
|
CustomerFeedbackSimple,
|
||||||
|
} from '@woocommerce/customer-effort-score';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './completed-header.scss';
|
||||||
|
import HeaderImage from './completed-celebration-header.svg';
|
||||||
|
|
||||||
|
type TaskListCompletedHeaderProps = {
|
||||||
|
hideTasks: () => void;
|
||||||
|
keepTasks: () => void;
|
||||||
|
customerEffortScore: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
||||||
|
'woocommerce_admin_install_timestamp';
|
||||||
|
const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';
|
||||||
|
const CUSTOMER_EFFORT_SCORE_ACTION = 'store_setup';
|
||||||
|
const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
|
||||||
|
|
||||||
|
function getStoreAgeInWeeks( adminInstallTimestamp: number ) {
|
||||||
|
if ( adminInstallTimestamp === 0 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date.now() is ms since Unix epoch, adminInstallTimestamp is in
|
||||||
|
// seconds since Unix epoch.
|
||||||
|
const storeAgeInMs = Date.now() - adminInstallTimestamp * 1000;
|
||||||
|
const storeAgeInWeeks = Math.round( storeAgeInMs / WEEK );
|
||||||
|
|
||||||
|
return storeAgeInWeeks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TaskListCompletedHeader: React.FC< TaskListCompletedHeaderProps > = ( {
|
||||||
|
hideTasks,
|
||||||
|
keepTasks,
|
||||||
|
customerEffortScore,
|
||||||
|
} ) => {
|
||||||
|
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||||
|
const [ showCesModal, setShowCesModal ] = useState( false );
|
||||||
|
const [ hasSubmittedScore, setHasSubmittedScore ] = useState( false );
|
||||||
|
const [ score, setScore ] = useState( NaN );
|
||||||
|
const [ hideCustomerEffortScore, setHideCustomerEffortScore ] = useState(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
storeAgeInWeeks,
|
||||||
|
cesShownForActions,
|
||||||
|
canShowCustomerEffortScore,
|
||||||
|
} = useSelect( ( select: WCDataSelector ) => {
|
||||||
|
const { getOption, hasFinishedResolution } = select(
|
||||||
|
OPTIONS_STORE_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( customerEffortScore ) {
|
||||||
|
const allowTracking = getOption( ALLOW_TRACKING_OPTION_NAME );
|
||||||
|
const adminInstallTimestamp: number =
|
||||||
|
getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0;
|
||||||
|
const cesActions = getOption< string[] >(
|
||||||
|
SHOWN_FOR_ACTIONS_OPTION_NAME
|
||||||
|
);
|
||||||
|
const loadingOptions =
|
||||||
|
! hasFinishedResolution( 'getOption', [
|
||||||
|
SHOWN_FOR_ACTIONS_OPTION_NAME,
|
||||||
|
] ) ||
|
||||||
|
! hasFinishedResolution( 'getOption', [
|
||||||
|
ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
|
||||||
|
] );
|
||||||
|
return {
|
||||||
|
storeAgeInWeeks: getStoreAgeInWeeks( adminInstallTimestamp ),
|
||||||
|
cesShownForActions: cesActions,
|
||||||
|
canShowCustomerEffortScore:
|
||||||
|
! loadingOptions &&
|
||||||
|
allowTracking &&
|
||||||
|
! ( cesActions || [] ).includes( 'store_setup' ),
|
||||||
|
loading: loadingOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
} );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if ( hasSubmittedScore ) {
|
||||||
|
setTimeout( () => {
|
||||||
|
setHideCustomerEffortScore( true );
|
||||||
|
}, 1200 );
|
||||||
|
}
|
||||||
|
}, [ hasSubmittedScore ] );
|
||||||
|
|
||||||
|
const submitScore = ( recordedScore: number, comments?: string ) => {
|
||||||
|
recordEvent( 'ces_feedback', {
|
||||||
|
action: CUSTOMER_EFFORT_SCORE_ACTION,
|
||||||
|
score: recordedScore,
|
||||||
|
comments: comments || '',
|
||||||
|
store_age: storeAgeInWeeks,
|
||||||
|
} );
|
||||||
|
updateOptions( {
|
||||||
|
[ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [
|
||||||
|
CUSTOMER_EFFORT_SCORE_ACTION,
|
||||||
|
...( cesShownForActions || [] ),
|
||||||
|
],
|
||||||
|
} );
|
||||||
|
setHasSubmittedScore( true );
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordScore = ( recordedScore: number ) => {
|
||||||
|
if ( recordedScore > 2 ) {
|
||||||
|
setScore( recordedScore );
|
||||||
|
submitScore( recordedScore );
|
||||||
|
} else {
|
||||||
|
setScore( recordedScore );
|
||||||
|
setShowCesModal( true );
|
||||||
|
recordEvent( 'ces_view', {
|
||||||
|
action: CUSTOMER_EFFORT_SCORE_ACTION,
|
||||||
|
store_age: storeAgeInWeeks,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordModalScore = ( recordedScore: number, comments: string ) => {
|
||||||
|
setShowCesModal( false );
|
||||||
|
submitScore( recordedScore, comments );
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={ classnames(
|
||||||
|
'woocommerce-task-dashboard__container two-column-experiment'
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
size="large"
|
||||||
|
className="woocommerce-task-card woocommerce-homescreen-card completed"
|
||||||
|
>
|
||||||
|
<CardHeader size="medium">
|
||||||
|
<div className="wooocommerce-task-card__header">
|
||||||
|
<img
|
||||||
|
src={ HeaderImage }
|
||||||
|
alt="Completed"
|
||||||
|
className="wooocommerce-task-card__finished-header-image"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size="title" as="h2" lineHeight={ 1.4 }>
|
||||||
|
{ __(
|
||||||
|
"You've completed store setup",
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="subtitle.small"
|
||||||
|
as="p"
|
||||||
|
size="13"
|
||||||
|
lineHeight="16px"
|
||||||
|
className="wooocommerce-task-card__header-subtitle"
|
||||||
|
>
|
||||||
|
{ __(
|
||||||
|
'Congratulations! Take a moment to celebrate and look out for the first sale.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Text>
|
||||||
|
<div className="woocommerce-task-card__header-menu">
|
||||||
|
<EllipsisMenu
|
||||||
|
label={ __(
|
||||||
|
'Task List Options',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
renderContent={ () => (
|
||||||
|
<div className="woocommerce-task-card__section-controls">
|
||||||
|
<Button
|
||||||
|
onClick={ () => keepTasks() }
|
||||||
|
>
|
||||||
|
{ __(
|
||||||
|
'Show setup task list',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={ () => hideTasks() }
|
||||||
|
>
|
||||||
|
{ __(
|
||||||
|
'Hide this',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
{ canShowCustomerEffortScore &&
|
||||||
|
! hideCustomerEffortScore &&
|
||||||
|
! hasSubmittedScore && (
|
||||||
|
<CustomerFeedbackSimple
|
||||||
|
label={ __(
|
||||||
|
'How was your experience?',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
onSelect={ recordScore }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
{ hasSubmittedScore && ! hideCustomerEffortScore && (
|
||||||
|
<div className="wooocommerce-task-card__header-ces-feedback">
|
||||||
|
<Text
|
||||||
|
variant="subtitle.small"
|
||||||
|
as="p"
|
||||||
|
size="13"
|
||||||
|
lineHeight="16px"
|
||||||
|
>
|
||||||
|
🙌{ ' ' }
|
||||||
|
{ __(
|
||||||
|
'We appreciate your feedback!',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{ showCesModal ? (
|
||||||
|
<CustomerFeedbackModal
|
||||||
|
label={ __( 'How was your experience?', 'woocommerce' ) }
|
||||||
|
defaultScore={ score }
|
||||||
|
recordScoreCallback={ recordModalScore }
|
||||||
|
onCloseModal={ () => {
|
||||||
|
setScore( NaN );
|
||||||
|
setShowCesModal( false );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) : null }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -89,6 +89,7 @@ const TaskDashboard = ( { query, twoColumns } ) => {
|
||||||
dismissedTasks={ dismissedTasks || [] }
|
dismissedTasks={ dismissedTasks || [] }
|
||||||
isComplete={ isTaskListComplete }
|
isComplete={ isTaskListComplete }
|
||||||
query={ query }
|
query={ query }
|
||||||
|
cesHeader={ false }
|
||||||
tasks={ setupTasks }
|
tasks={ setupTasks }
|
||||||
title={ __( 'Get ready to start selling', 'woocommerce' ) }
|
title={ __( 'Get ready to start selling', 'woocommerce' ) }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { TaskListProps } from '~/tasks/task-list';
|
||||||
import { ProgressHeader } from '~/task-lists/progress-header';
|
import { ProgressHeader } from '~/task-lists/progress-header';
|
||||||
import { SectionPanelTitle } from './section-panel-title';
|
import { SectionPanelTitle } from './section-panel-title';
|
||||||
import { TaskListItem } from './task-list-item';
|
import { TaskListItem } from './task-list-item';
|
||||||
|
import { TaskListCompletedHeader } from './completed-header';
|
||||||
|
|
||||||
type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & {
|
type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & {
|
||||||
title: string | React.ReactNode | undefined;
|
title: string | React.ReactNode | undefined;
|
||||||
|
@ -40,6 +41,7 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( {
|
||||||
isComplete,
|
isComplete,
|
||||||
sections,
|
sections,
|
||||||
displayProgressHeader,
|
displayProgressHeader,
|
||||||
|
cesHeader = true,
|
||||||
} ) => {
|
} ) => {
|
||||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||||
const { profileItems } = useSelect( ( select ) => {
|
const { profileItems } = useSelect( ( select ) => {
|
||||||
|
@ -115,14 +117,22 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( {
|
||||||
return <div className="woocommerce-task-dashboard__container"></div>;
|
return <div className="woocommerce-task-dashboard__container"></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isComplete && ! keepCompletedTaskList ) {
|
if ( isComplete && keepCompletedTaskList !== 'yes' ) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{ cesHeader ? (
|
||||||
|
<TaskListCompletedHeader
|
||||||
|
hideTasks={ hideTasks }
|
||||||
|
keepTasks={ keepTasks }
|
||||||
|
customerEffortScore={ true }
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<TaskListCompleted
|
<TaskListCompleted
|
||||||
hideTasks={ hideTasks }
|
hideTasks={ hideTasks }
|
||||||
keepTasks={ keepTasks }
|
keepTasks={ keepTasks }
|
||||||
twoColumns={ false }
|
twoColumns={ false }
|
||||||
/>
|
/>
|
||||||
|
) }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,9 +81,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5em;
|
margin-top: $gap-large;
|
||||||
font-weight: normal;
|
margin-bottom: $gap-small;
|
||||||
margin-top: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wooocommerce-task-card__header {
|
.wooocommerce-task-card__header {
|
||||||
|
@ -92,7 +91,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
button.is-secondary {
|
button.is-secondary {
|
||||||
margin-right: 12px;
|
margin-right: $gap-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
useUserPreferences,
|
useUserPreferences,
|
||||||
getVisibleTasks,
|
getVisibleTasks,
|
||||||
TaskListType,
|
TaskListType,
|
||||||
|
WCDataSelector,
|
||||||
} from '@woocommerce/data';
|
} from '@woocommerce/data';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { List } from '@woocommerce/experimental';
|
import { List } from '@woocommerce/experimental';
|
||||||
|
@ -33,6 +34,7 @@ import DismissModal from './dismiss-modal';
|
||||||
import TaskListCompleted from './completed';
|
import TaskListCompleted from './completed';
|
||||||
import { ProgressHeader } from '~/task-lists/progress-header';
|
import { ProgressHeader } from '~/task-lists/progress-header';
|
||||||
import { TaskListItemTwoColumn } from './task-list-item-two-column';
|
import { TaskListItemTwoColumn } from './task-list-item-two-column';
|
||||||
|
import { TaskListCompletedHeader } from './completed-header';
|
||||||
|
|
||||||
export type TaskListProps = TaskListType & {
|
export type TaskListProps = TaskListType & {
|
||||||
eventName?: string;
|
eventName?: string;
|
||||||
|
@ -40,6 +42,7 @@ export type TaskListProps = TaskListType & {
|
||||||
query: {
|
query: {
|
||||||
task?: string;
|
task?: string;
|
||||||
};
|
};
|
||||||
|
cesHeader?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TaskList: React.FC< TaskListProps > = ( {
|
export const TaskList: React.FC< TaskListProps > = ( {
|
||||||
|
@ -52,16 +55,20 @@ export const TaskList: React.FC< TaskListProps > = ( {
|
||||||
keepCompletedTaskList,
|
keepCompletedTaskList,
|
||||||
isComplete,
|
isComplete,
|
||||||
displayProgressHeader,
|
displayProgressHeader,
|
||||||
|
cesHeader = true,
|
||||||
} ) => {
|
} ) => {
|
||||||
const listEventPrefix = eventName ? eventName + '_' : eventPrefix;
|
const listEventPrefix = eventName ? eventName + '_' : eventPrefix;
|
||||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
const { profileItems } = useSelect( ( select: WCDataSelector ) => {
|
||||||
const { profileItems } = useSelect( ( select ) => {
|
|
||||||
const { getProfileItems } = select( ONBOARDING_STORE_NAME );
|
const { getProfileItems } = select( ONBOARDING_STORE_NAME );
|
||||||
return {
|
return {
|
||||||
profileItems: getProfileItems(),
|
profileItems: getProfileItems(),
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
const { hideTaskList, visitedTask } = useDispatch( ONBOARDING_STORE_NAME );
|
const {
|
||||||
|
hideTaskList,
|
||||||
|
visitedTask,
|
||||||
|
keepCompletedTaskList: keepCompletedTasks,
|
||||||
|
} = useDispatch( ONBOARDING_STORE_NAME );
|
||||||
const userPreferences = useUserPreferences();
|
const userPreferences = useUserPreferences();
|
||||||
const [ headerData, setHeaderData ] = useState< {
|
const [ headerData, setHeaderData ] = useState< {
|
||||||
task?: TaskType;
|
task?: TaskType;
|
||||||
|
@ -108,13 +115,7 @@ export const TaskList: React.FC< TaskListProps > = ( {
|
||||||
};
|
};
|
||||||
|
|
||||||
const keepTasks = () => {
|
const keepTasks = () => {
|
||||||
const updateOptionsParams = {
|
keepCompletedTasks( id );
|
||||||
woocommerce_task_list_keep_completed: 'yes',
|
|
||||||
};
|
|
||||||
|
|
||||||
updateOptions( {
|
|
||||||
...updateOptionsParams,
|
|
||||||
} );
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMenu = () => {
|
const renderMenu = () => {
|
||||||
|
@ -226,14 +227,22 @@ export const TaskList: React.FC< TaskListProps > = ( {
|
||||||
return <div className="woocommerce-task-dashboard__container"></div>;
|
return <div className="woocommerce-task-dashboard__container"></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isComplete && ! keepCompletedTaskList ) {
|
if ( isComplete && keepCompletedTaskList !== 'yes' ) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{ cesHeader ? (
|
||||||
|
<TaskListCompletedHeader
|
||||||
|
hideTasks={ hideTasks }
|
||||||
|
keepTasks={ keepTasks }
|
||||||
|
customerEffortScore={ true }
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<TaskListCompleted
|
<TaskListCompleted
|
||||||
hideTasks={ hideTasks }
|
hideTasks={ hideTasks }
|
||||||
keepTasks={ keepTasks }
|
keepTasks={ keepTasks }
|
||||||
twoColumns={ false }
|
twoColumns={ false }
|
||||||
/>
|
/>
|
||||||
|
) }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue