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:
louwie17 2022-05-17 17:39:14 -03:00 committed by GitHub
commit e822a4a7f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 641 additions and 72 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new simple customer feedback component for inline CES feedback. #32538

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add TypeScript type support as part of the build process. #32538

View File

@ -18,6 +18,7 @@
},
"main": "build/index.js",
"module": "build-module/index.js",
"types": "build-types",
"react-native": "src/index",
"dependencies": {
"@woocommerce/experimental": "workspace:*",

View File

@ -4,13 +4,12 @@
import { createElement, useState, useEffect } from '@wordpress/element';
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import CustomerFeedbackModal from './customer-feedback-modal';
import { CustomerFeedbackModal } from './customer-feedback-modal';
const noop = () => {};
@ -23,16 +22,14 @@ const noop = () => {};
* @param {Object} props Component props.
* @param {Function} props.recordScoreCallback Function to call when the score should be recorded.
* @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.onNoticeDismissedCallback Function to call when the notice is dismissed.
* @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.
*/
export function CustomerEffortScore( {
function CustomerEffortScore( {
recordScoreCallback,
label,
createNotice,
onNoticeShownCallback = noop,
onNoticeDismissedCallback = noop,
onModalShownCallback = noop,
@ -40,6 +37,7 @@ export function CustomerEffortScore( {
} ) {
const [ shouldCreateNotice, setShouldCreateNotice ] = useState( true );
const [ visible, setVisible ] = useState( false );
const { createNotice } = useDispatch( 'core/notices2' );
useEffect( () => {
if ( ! shouldCreateNotice ) {
@ -91,10 +89,6 @@ CustomerEffortScore.propTypes = {
* The label displayed in the modal.
*/
label: PropTypes.string.isRequired,
/**
* Create a notice (snackbar).
*/
createNotice: PropTypes.func.isRequired,
/**
* The function to call when the notice is shown.
*/
@ -113,12 +107,4 @@ CustomerEffortScore.propTypes = {
icon: PropTypes.element,
};
export default compose(
withDispatch( ( dispatch ) => {
const { createNotice } = dispatch( 'core/notices2' );
return {
createNotice,
};
} )
)( CustomerEffortScore );
export { CustomerEffortScore };

View File

@ -26,13 +26,19 @@ import { __ } from '@wordpress/i18n';
* @param {Object} props Component props.
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
* @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( {
recordScoreCallback,
label,
defaultScore = NaN,
onCloseModal,
}: {
recordScoreCallback: ( score: number, comments: string ) => void;
label: string;
defaultScore?: number;
onCloseModal?: () => void;
} ): JSX.Element | null {
const options = [
{
@ -57,12 +63,17 @@ function CustomerFeedbackModal( {
},
];
const [ score, setScore ] = useState( NaN );
const [ score, setScore ] = useState( defaultScore || NaN );
const [ comments, setComments ] = useState( '' );
const [ showNoScoreMessage, setShowNoScoreMessage ] = useState( false );
const [ isOpen, setOpen ] = useState( true );
const closeModal = () => setOpen( false );
const closeModal = () => {
setOpen( false );
if ( onCloseModal ) {
onCloseModal();
}
};
const onRadioControlChange = ( value: string ) => {
const valueAsInt = parseInt( value, 10 );
@ -111,7 +122,7 @@ function CustomerFeedbackModal( {
{ ( score === 1 || score === 2 ) && (
<div className="woocommerce-customer-effort-score__comments">
<TextareaControl
label={ __( 'Comments (Optional)', 'woocommerce' ) }
label={ __( 'Comments (optional)', 'woocommerce' ) }
help={ __(
'Your feedback will go to the WooCommerce development team',
'woocommerce'
@ -152,6 +163,8 @@ function CustomerFeedbackModal( {
CustomerFeedbackModal.propTypes = {
recordScoreCallback: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
defaultScore: PropTypes.number,
onCloseModal: PropTypes.func,
};
export default CustomerFeedbackModal;
export { CustomerFeedbackModal };

View File

@ -7,7 +7,7 @@ import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import CustomerFeedbackModal from '../index';
import { CustomerFeedbackModal } from '../index';
const mockRecordScoreCallback = jest.fn();
@ -59,7 +59,7 @@ describe( 'CustomerFeedbackModal', () => {
await screen.findByRole( 'dialog' );
expect(
screen.queryByLabelText( 'Comments (Optional)' )
screen.queryByLabelText( 'Comments (optional)' )
).not.toBeInTheDocument();
} );
@ -80,7 +80,7 @@ describe( 'CustomerFeedbackModal', () => {
fireEvent.click( screen.getByLabelText( labelText ) );
// Wait for comments field to show.
await screen.findByLabelText( 'Comments (Optional)' );
await screen.findByLabelText( 'Comments (optional)' );
// Select neutral score.
fireEvent.click( screen.getByLabelText( 'Neutral' ) );
@ -88,7 +88,7 @@ describe( 'CustomerFeedbackModal', () => {
// Wait for comments field to hide.
await waitFor( () => {
expect(
screen.queryByLabelText( 'Comments (Optional)' )
screen.queryByLabelText( 'Comments (optional)' )
).not.toBeInTheDocument();
} );
}

View File

@ -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;
}

View File

@ -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 };

View File

@ -0,0 +1 @@
export * from './customer-feedback-simple';

View File

@ -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 );
} );
} );

View File

@ -0,0 +1,3 @@
export * from './customer-effort-score';
export * from './customer-feedback-simple';
export * from './customer-feedback-modal';

View File

@ -1,3 +1,5 @@
@import 'customer-feedback-simple/customer-feedback-simple.scss';
.woocommerce-customer-effort-score__selection {
margin: 1em 0;
@ -68,7 +70,7 @@
}
input[value='3'] + label::before {
content: '😐';
content: '😑';
}
input[value='4'] + label::before {
@ -76,7 +78,7 @@
}
input[value='5'] + label::before {
content: '😁';
content: '😍';
}
}
}

View File

@ -3,22 +3,37 @@
*/
import { render, screen } from '@testing-library/react';
import { createElement } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { CustomerEffortScore } from '../index';
import { CustomerEffortScore } from '../customer-effort-score';
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', () => {
it( 'should call createNotice with appropriate parameters', async () => {
const mockCreateNotice = jest.fn();
useDispatch.mockReturnValue( {
createNotice: mockCreateNotice,
} );
const icon = <span>icon</span>;
render(
<CustomerEffortScore
createNotice={ mockCreateNotice }
recordScoreCallback={ noop }
label={ 'label' }
onNoticeDismissedCallback={ noop }
@ -41,6 +56,9 @@ describe( 'CustomerEffortScore', () => {
it( 'should not call createNotice on rerender', async () => {
const mockCreateNotice = jest.fn();
useDispatch.mockReturnValue( {
createNotice: mockCreateNotice,
} );
const { rerender } = render(
<CustomerEffortScore
@ -53,7 +71,6 @@ describe( 'CustomerEffortScore', () => {
// Simulate rerender by changing label prop.
rerender(
<CustomerEffortScore
createNotice={ mockCreateNotice }
recordScoreCallback={ noop }
label={ 'label2' }
/>
@ -65,7 +82,6 @@ describe( 'CustomerEffortScore', () => {
it( 'should not show dialog if no action is taken', async () => {
render(
<CustomerEffortScore
createNotice={ noop }
recordScoreCallback={ noop }
label={ 'label' }
/>
@ -91,10 +107,12 @@ describe( 'CustomerEffortScore', () => {
// Modal shown callback should also be called.
expect( mockOnModalShownCallback ).toHaveBeenCalled();
};
useDispatch.mockReturnValue( {
createNotice,
} );
render(
<CustomerEffortScore
createNotice={ createNotice }
recordScoreCallback={ noop }
label={ 'label' }
onModalShownCallback={ mockOnModalShownCallback }

View File

@ -2,6 +2,9 @@
"extends": "../tsconfig",
"compilerOptions": {
"rootDir": "src",
"outDir": "build-module"
"outDir": "build-module",
"declaration": true,
"declarationMap": true,
"declarationDir": "./build-types"
}
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Added TypeScript options selectors and action in onboarding store for keeping the completed task list. #32158

View File

@ -105,6 +105,7 @@ import { WPDataSelectors } from './types';
import { PaymentSelectors } from './payment-gateways/selectors';
import { PluginSelectors } from './plugins/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
// 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
? WPDataSelectors
: T extends typeof OPTIONS_STORE_NAME
? WPDataSelectors
? OptionsSelectors
: T extends typeof NAVIGATION_STORE_NAME
? WPDataSelectors
: T extends typeof NOTES_STORE_NAME

View File

@ -34,6 +34,8 @@ const TYPES = {
ACTION_TASK_REQUEST: 'ACTION_TASK_REQUEST',
ACTION_TASK_SUCCESS: 'ACTION_TASK_SUCCESS',
VISITED_TASK: 'VISITED_TASK',
KEEP_COMPLETED_TASKS_REQUEST: 'KEEP_COMPLETED_TASKS_REQUEST',
KEEP_COMPLETED_TASKS_SUCCESS: 'KEEP_COMPLETED_TASKS_SUCCESS',
};
export default TYPES;

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
import { controls } from '@wordpress/data';
/**
* Internal dependencies
@ -9,6 +10,7 @@ import { apiFetch } from '@wordpress/data-controls';
import TYPES from './action-types';
import { WC_ADMIN_NAMESPACE } from '../constants';
import { DeprecatedTasks } from './deprecated-tasks';
import { STORE_NAME as OPTIONS_STORE_NAME } from '../options/constants';
export function getFreeExtensionsError( error ) {
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 ) {
return {
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 ) {
yield setIsRequesting( 'updateProfileItems', true );
yield setError( 'updateProfileItems', null );

View File

@ -72,6 +72,7 @@ const onboarding = (
taskListId,
taskList,
taskLists,
keepCompletedTaskList,
}
) => {
switch ( type ) {
@ -372,6 +373,17 @@ const onboarding = (
[ taskListId ]: taskList,
},
};
case TYPES.KEEP_COMPLETED_TASKS_SUCCESS:
return {
...state,
taskLists: {
...state.taskLists,
[ taskListId ]: {
...state.taskLists[ taskListId ],
keepCompletedTaskList,
},
},
};
case TYPES.OPTIMISTICALLY_COMPLETE_TASK_REQUEST:
return {
...state,

View File

@ -51,6 +51,7 @@ export type TaskListType = {
eventPrefix: string;
displayProgressHeader: boolean;
keepCompletedTaskList: 'yes' | 'no';
showCESFeedback?: boolean;
sections?: TaskListSection[];
isToggleable?: boolean;
isCollapsible?: boolean;

View File

@ -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;

View File

@ -4,7 +4,7 @@
import { useState } from '@wordpress/element';
import PropTypes from 'prop-types';
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 { withSelect, withDispatch } from '@wordpress/data';
import { OPTIONS_STORE_NAME, WEEK } from '@woocommerce/data';
@ -117,7 +117,7 @@ function CustomerEffortScoreTracks( {
return (
<CustomerEffortScore
recordScoreCallback={ recordScore }
onSelect={ recordScore }
label={ label }
onNoticeShownCallback={ onNoticeShown }
onNoticeDismissedCallback={ onNoticeDismissed }

View File

@ -26,6 +26,10 @@ export type TaskListProps = TaskListType & {
query: {
task?: string;
};
eventName?: string;
twoColumns?: boolean;
keepCompletedTaskList?: 'yes' | 'no';
cesHeader?: boolean;
};
export const TaskList: React.FC< TaskListProps > = ( {

View File

@ -11,6 +11,7 @@ import {
OPTIONS_STORE_NAME,
TaskListType,
TaskType,
WCDataSelector,
} from '@woocommerce/data';
import { useExperiment } from '@woocommerce/explat';
import { recordEvent } from '@woocommerce/tracks';
@ -66,14 +67,16 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => {
'woocommerce_tasklist_progression'
);
const { isResolving, taskLists } = useSelect( ( select ) => {
const { isResolving, taskLists } = useSelect(
( select: WCDataSelector ) => {
return {
isResolving: ! select(
ONBOARDING_STORE_NAME
).hasFinishedResolution( 'getTaskLists' ),
taskLists: select( ONBOARDING_STORE_NAME ).getTaskLists(),
};
} );
}
);
const getCurrentTask = () => {
if ( ! task ) {

View File

@ -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

View File

@ -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;
}

View File

@ -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 }
</>
);
};

View File

@ -89,6 +89,7 @@ const TaskDashboard = ( { query, twoColumns } ) => {
dismissedTasks={ dismissedTasks || [] }
isComplete={ isTaskListComplete }
query={ query }
cesHeader={ false }
tasks={ setupTasks }
title={ __( 'Get ready to start selling', 'woocommerce' ) }
/>

View File

@ -24,6 +24,7 @@ import { TaskListProps } from '~/tasks/task-list';
import { ProgressHeader } from '~/task-lists/progress-header';
import { SectionPanelTitle } from './section-panel-title';
import { TaskListItem } from './task-list-item';
import { TaskListCompletedHeader } from './completed-header';
type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & {
title: string | React.ReactNode | undefined;
@ -40,6 +41,7 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( {
isComplete,
sections,
displayProgressHeader,
cesHeader = true,
} ) => {
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { profileItems } = useSelect( ( select ) => {
@ -115,14 +117,22 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( {
return <div className="woocommerce-task-dashboard__container"></div>;
}
if ( isComplete && ! keepCompletedTaskList ) {
if ( isComplete && keepCompletedTaskList !== 'yes' ) {
return (
<>
{ cesHeader ? (
<TaskListCompletedHeader
hideTasks={ hideTasks }
keepTasks={ keepTasks }
customerEffortScore={ true }
/>
) : (
<TaskListCompleted
hideTasks={ hideTasks }
keepTasks={ keepTasks }
twoColumns={ false }
/>
) }
</>
);
}

View File

@ -81,9 +81,8 @@
}
h2 {
font-size: 1.5em;
font-weight: normal;
margin-top: 22px;
margin-top: $gap-large;
margin-bottom: $gap-small;
}
.wooocommerce-task-card__header {
@ -92,7 +91,7 @@
}
button.is-secondary {
margin-right: 12px;
margin-right: $gap-small;
}
}

View File

@ -18,6 +18,7 @@ import {
useUserPreferences,
getVisibleTasks,
TaskListType,
WCDataSelector,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { List } from '@woocommerce/experimental';
@ -33,6 +34,7 @@ import DismissModal from './dismiss-modal';
import TaskListCompleted from './completed';
import { ProgressHeader } from '~/task-lists/progress-header';
import { TaskListItemTwoColumn } from './task-list-item-two-column';
import { TaskListCompletedHeader } from './completed-header';
export type TaskListProps = TaskListType & {
eventName?: string;
@ -40,6 +42,7 @@ export type TaskListProps = TaskListType & {
query: {
task?: string;
};
cesHeader?: boolean;
};
export const TaskList: React.FC< TaskListProps > = ( {
@ -52,16 +55,20 @@ export const TaskList: React.FC< TaskListProps > = ( {
keepCompletedTaskList,
isComplete,
displayProgressHeader,
cesHeader = true,
} ) => {
const listEventPrefix = eventName ? eventName + '_' : eventPrefix;
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { profileItems } = useSelect( ( select ) => {
const { profileItems } = useSelect( ( select: WCDataSelector ) => {
const { getProfileItems } = select( ONBOARDING_STORE_NAME );
return {
profileItems: getProfileItems(),
};
} );
const { hideTaskList, visitedTask } = useDispatch( ONBOARDING_STORE_NAME );
const {
hideTaskList,
visitedTask,
keepCompletedTaskList: keepCompletedTasks,
} = useDispatch( ONBOARDING_STORE_NAME );
const userPreferences = useUserPreferences();
const [ headerData, setHeaderData ] = useState< {
task?: TaskType;
@ -108,13 +115,7 @@ export const TaskList: React.FC< TaskListProps > = ( {
};
const keepTasks = () => {
const updateOptionsParams = {
woocommerce_task_list_keep_completed: 'yes',
};
updateOptions( {
...updateOptionsParams,
} );
keepCompletedTasks( id );
};
const renderMenu = () => {
@ -226,14 +227,22 @@ export const TaskList: React.FC< TaskListProps > = ( {
return <div className="woocommerce-task-dashboard__container"></div>;
}
if ( isComplete && ! keepCompletedTaskList ) {
if ( isComplete && keepCompletedTaskList !== 'yes' ) {
return (
<>
{ cesHeader ? (
<TaskListCompletedHeader
hideTasks={ hideTasks }
keepTasks={ keepTasks }
customerEffortScore={ true }
/>
) : (
<TaskListCompleted
hideTasks={ hideTasks }
keepTasks={ keepTasks }
twoColumns={ false }
/>
) }
</>
);
}