From 4db5c0d8cdce89e69cf730c5057b6cff7cebe66a Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Tue, 29 Nov 2022 08:15:58 -0800 Subject: [PATCH] Add additional question to CES modal (#35680) --- .../changelog/add-35124-ces-add-question | 4 + .../src/customer-effort-score.tsx | 28 +++-- .../src/customer-feedback-modal/index.tsx | 114 +++++++++++++++--- .../customer-feedback-modal/test/index.tsx | 34 ++++-- .../js/customer-effort-score/src/style.scss | 11 +- .../customer-effort-score/src/test/index.tsx | 22 +++- .../customer-effort-score-tracks-container.js | 4 +- .../customer-effort-score-tracks.js | 16 ++- .../data/actions.js | 78 ++++++++---- .../data/reducer.js | 4 +- .../two-column-tasks/completed-header.tsx | 34 +++++- .../changelog/add-35124-ces-add-question | 4 + .../Admin/CustomerEffortScoreTracks.php | 90 ++++++++++++-- 13 files changed, 350 insertions(+), 93 deletions(-) create mode 100644 packages/js/customer-effort-score/changelog/add-35124-ces-add-question create mode 100644 plugins/woocommerce/changelog/add-35124-ces-add-question diff --git a/packages/js/customer-effort-score/changelog/add-35124-ces-add-question b/packages/js/customer-effort-score/changelog/add-35124-ces-add-question new file mode 100644 index 00000000000..e89521d9d87 --- /dev/null +++ b/packages/js/customer-effort-score/changelog/add-35124-ces-add-question @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Updating to accept two questions to display in CES modal. diff --git a/packages/js/customer-effort-score/src/customer-effort-score.tsx b/packages/js/customer-effort-score/src/customer-effort-score.tsx index 958da108eae..34725585e5a 100644 --- a/packages/js/customer-effort-score/src/customer-effort-score.tsx +++ b/packages/js/customer-effort-score/src/customer-effort-score.tsx @@ -14,8 +14,14 @@ import { CustomerFeedbackModal } from './customer-feedback-modal'; const noop = () => {}; type CustomerEffortScoreProps = { - recordScoreCallback: ( score: number, comments: string ) => void; - label: string; + recordScoreCallback: ( + score: number, + secondScore: number, + comments: string + ) => void; + title: string; + firstQuestion: string; + secondQuestion: string; onNoticeShownCallback?: () => void; onNoticeDismissedCallback?: () => void; onModalShownCallback?: () => void; @@ -30,7 +36,9 @@ type CustomerEffortScoreProps = { * * @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 {string} props.title The title displayed in the modal. + * @param {string} props.firstQuestion The first survey question. + * @param {string} props.secondQuestion The second survey question. * @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. @@ -38,7 +46,9 @@ type CustomerEffortScoreProps = { */ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( { recordScoreCallback, - label, + title, + firstQuestion, + secondQuestion, onNoticeShownCallback = noop, onNoticeDismissedCallback = noop, onModalShownCallback = noop, @@ -53,7 +63,7 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( { return; } - createNotice( 'success', label, { + createNotice( 'success', title, { actions: [ { label: __( 'Give feedback', 'woocommerce' ), @@ -83,7 +93,9 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( { return ( ); @@ -95,9 +107,9 @@ CustomerEffortScore.propTypes = { */ recordScoreCallback: PropTypes.func.isRequired, /** - * The label displayed in the modal. + * The title displayed in the modal. */ - label: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, /** * The function to call when the notice is shown. */ diff --git a/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx b/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx index f167740a90a..7f060b696d7 100644 --- a/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx +++ b/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx @@ -25,18 +25,28 @@ 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.title Title displayed in the modal. + * @param {string} props.firstQuestion The first survey question. + * @param {string} props.secondQuestion The second survey question. * @param {string} props.defaultScore Default score. * @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel. */ function CustomerFeedbackModal( { recordScoreCallback, - label, + title, + firstQuestion, + secondQuestion, defaultScore = NaN, onCloseModal, }: { - recordScoreCallback: ( score: number, comments: string ) => void; - label: string; + recordScoreCallback: ( + score: number, + secondScore: number, + comments: string + ) => void; + title: string; + firstQuestion: string; + secondQuestion: string; defaultScore?: number; onCloseModal?: () => void; } ): JSX.Element | null { @@ -63,7 +73,12 @@ function CustomerFeedbackModal( { }, ]; - const [ score, setScore ] = useState( defaultScore || NaN ); + const [ firstQuestionScore, setFirstQuestionScore ] = useState( + defaultScore || NaN + ); + const [ secondQuestionScore, setSecondQuestionScore ] = useState( + defaultScore || NaN + ); const [ comments, setComments ] = useState( '' ); const [ showNoScoreMessage, setShowNoScoreMessage ] = useState( false ); const [ isOpen, setOpen ] = useState( true ); @@ -75,19 +90,31 @@ function CustomerFeedbackModal( { } }; - const onRadioControlChange = ( value: string ) => { + const onRadioControlChange = ( + value: string, + setter: ( val: number ) => void + ) => { const valueAsInt = parseInt( value, 10 ); - setScore( valueAsInt ); + setter( valueAsInt ); setShowNoScoreMessage( ! Number.isInteger( valueAsInt ) ); }; const sendScore = () => { - if ( ! Number.isInteger( score ) ) { + if ( + ! ( + Number.isInteger( firstQuestionScore ) && + Number.isInteger( secondQuestionScore ) + ) + ) { setShowNoScoreMessage( true ); return; } setOpen( false ); - recordScoreCallback( score, comments ); + recordScoreCallback( + firstQuestionScore, + secondQuestionScore, + comments + ); }; if ( ! isOpen ) { @@ -97,10 +124,24 @@ function CustomerFeedbackModal( { return ( + + { __( + 'Your feedback will help create a better experience for thousands of merchants like you. Please tell us to what extent you agree or disagree with the statements below.', + 'woocommerce' + ) } + + - { label } + { firstQuestion }
+ onRadioControlChange( + value as string, + setFirstQuestionScore + ) + } />
- { ( score === 1 || score === 2 ) && ( + + { secondQuestion } + + +
+ + onRadioControlChange( + value as string, + setSecondQuestionScore + ) + } + /> +
+ + { [ firstQuestionScore, secondQuestionScore ].some( + ( score ) => score === 1 || score === 2 + ) && (
setComments( value ) } rows={ 5 } /> @@ -153,7 +231,7 @@ function CustomerFeedbackModal( { { __( 'Cancel', 'woocommerce' ) }
@@ -162,7 +240,9 @@ function CustomerFeedbackModal( { CustomerFeedbackModal.propTypes = { recordScoreCallback: PropTypes.func.isRequired, - label: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + firstQuestion: PropTypes.string.isRequired, + secondQuestion: PropTypes.string.isRequired, defaultScore: PropTypes.number, onCloseModal: PropTypes.func, }; diff --git a/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx b/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx index 40829821a4e..2604b415ee7 100644 --- a/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx +++ b/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx @@ -16,7 +16,9 @@ describe( 'CustomerFeedbackModal', () => { render( ); @@ -33,13 +35,15 @@ describe( 'CustomerFeedbackModal', () => { render( ); await screen.findByRole( 'dialog' ); // Wait for the modal to render. - fireEvent.click( screen.getByRole( 'button', { name: /send/i } ) ); // Press send button. + fireEvent.click( screen.getByRole( 'button', { name: /share/i } ) ); // Press send button. // Wait for error message. await screen.findByRole( 'alert' ); @@ -51,7 +55,9 @@ describe( 'CustomerFeedbackModal', () => { render( ); @@ -59,7 +65,9 @@ describe( 'CustomerFeedbackModal', () => { await screen.findByRole( 'dialog' ); expect( - screen.queryByLabelText( 'Comments (optional)' ) + screen.queryByLabelText( + 'How is that screen useful to you? What features would you add or change?' + ) ).not.toBeInTheDocument(); } ); @@ -69,7 +77,9 @@ describe( 'CustomerFeedbackModal', () => { render( ); @@ -77,18 +87,22 @@ describe( 'CustomerFeedbackModal', () => { await screen.findByRole( 'dialog' ); // Select the option. - fireEvent.click( screen.getByLabelText( labelText ) ); + fireEvent.click( screen.getAllByLabelText( labelText )[ 0 ] ); // Wait for comments field to show. - await screen.findByLabelText( 'Comments (optional)' ); + await screen.findByLabelText( + 'How is that screen useful to you? What features would you add or change?' + ); // Select neutral score. - fireEvent.click( screen.getByLabelText( 'Neutral' ) ); + fireEvent.click( screen.getAllByLabelText( 'Neutral' )[ 0 ] ); // Wait for comments field to hide. await waitFor( () => { expect( - screen.queryByLabelText( 'Comments (optional)' ) + screen.queryByLabelText( + 'How is that screen useful to you? What features would you add or change?' + ) ).not.toBeInTheDocument(); } ); } diff --git a/packages/js/customer-effort-score/src/style.scss b/packages/js/customer-effort-score/src/style.scss index 7af35552e4a..adabb8fdba9 100644 --- a/packages/js/customer-effort-score/src/style.scss +++ b/packages/js/customer-effort-score/src/style.scss @@ -1,7 +1,7 @@ @import 'customer-feedback-simple/customer-feedback-simple.scss'; .woocommerce-customer-effort-score__selection { - margin: 1em 0; + margin: 1em 0 1.5em 0; .components-base-control__field { display: flex; @@ -91,10 +91,14 @@ } .woocommerce-customer-effort-score__comments { + margin-bottom: 1.5em; + label { display: block; color: inherit; font-weight: bold; + text-transform: none; + font-size: 14px; } textarea { @@ -109,3 +113,8 @@ margin-left: 1em; } } + +.woocommerce-customer-effort-score .woocommerce-customer-effort-score__intro { + max-width: 550px; + margin: 0 0 1.5em 0; +} diff --git a/packages/js/customer-effort-score/src/test/index.tsx b/packages/js/customer-effort-score/src/test/index.tsx index 59f54f2afce..e8bd5e774cc 100644 --- a/packages/js/customer-effort-score/src/test/index.tsx +++ b/packages/js/customer-effort-score/src/test/index.tsx @@ -35,7 +35,9 @@ describe( 'CustomerEffortScore', () => { render( @@ -45,7 +47,7 @@ describe( 'CustomerEffortScore', () => { // Notice status. expect.any( String ), // Notice message. - 'label', + 'title', // Notice options. expect.objectContaining( { icon, @@ -63,7 +65,9 @@ describe( 'CustomerEffortScore', () => { const { rerender } = render( ); @@ -71,7 +75,9 @@ describe( 'CustomerEffortScore', () => { rerender( ); @@ -82,7 +88,9 @@ describe( 'CustomerEffortScore', () => { render( ); @@ -121,7 +129,9 @@ describe( 'CustomerEffortScore', () => { render( ); diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js index 32fba5c39e6..42dae830326 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js @@ -49,7 +49,9 @@ function CustomerEffortScoreTracksContainer( { diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js index b55b39548e3..de458563360 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js @@ -22,7 +22,9 @@ const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking'; * @param {Object} props Component props. * @param {string} props.action The action name sent to Tracks. * @param {Object} props.trackProps Additional props sent to Tracks. - * @param {string} props.label The label displayed in the modal. + * @param {string} props.title The title displayed in the modal. + * @param {string} props.firstQuestion The first survey question. + * @param {string} props.secondQuestion The second survey question. * @param {string} props.onSubmitLabel The label displayed upon survey submission. * @param {Array} props.cesShownForActions The array of actions that the CES modal has been shown for. * @param {boolean} props.allowTracking Whether tracking is allowed or not. @@ -34,7 +36,9 @@ const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking'; function CustomerEffortScoreTracks( { action, trackProps, - label, + title, + firstQuestion, + secondQuestion, onSubmitLabel = __( 'Thank you for your feedback!', 'woocommerce' ), cesShownForActions, allowTracking, @@ -104,10 +108,12 @@ function CustomerEffortScoreTracks( { addActionToShownOption(); }; - const recordScore = ( score, comments ) => { + const recordScore = ( score, secondScore, comments ) => { recordEvent( 'ces_feedback', { action, score, + score_second_question: secondScore, + score_combined: score + secondScore, comments: comments || '', store_age: storeAgeInWeeks, ...trackProps, @@ -118,7 +124,9 @@ function CustomerEffortScoreTracks( { return ( { } const newTrack = { action: action.action, - label: action.label, + title: action.title, + firstQuestion: action.firstQuestion, + secondQuestion: action.secondQuestion, pagenow: action.pageNow, adminpage: action.adminPage, onSubmitLabel: action.onSubmitLabel, diff --git a/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx index 26eada04092..91e7623b723 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx @@ -97,10 +97,20 @@ export const TaskListCompletedHeader: React.FC< } }, [ hasSubmittedScore ] ); - const submitScore = ( recordedScore: number, comments?: string ) => { + const submitScore = ( { + firstScore, + secondScore, + comments, + }: { + firstScore: number; + secondScore?: number; + comments?: string; + } ) => { recordEvent( 'ces_feedback', { action: CUSTOMER_EFFORT_SCORE_ACTION, - score: recordedScore, + score: firstScore, + score_second_question: secondScore ?? null, + score_combined: firstScore + ( secondScore ?? 0 ), comments: comments || '', store_age: storeAgeInWeeks, } ); @@ -116,7 +126,7 @@ export const TaskListCompletedHeader: React.FC< const recordScore = ( recordedScore: number ) => { if ( recordedScore > 2 ) { setScore( recordedScore ); - submitScore( recordedScore ); + submitScore( { firstScore: recordedScore } ); } else { setScore( recordedScore ); setShowCesModal( true ); @@ -127,9 +137,13 @@ export const TaskListCompletedHeader: React.FC< } }; - const recordModalScore = ( recordedScore: number, comments: string ) => { + const recordModalScore = ( + firstScore: number, + secondScore: number, + comments: string + ) => { setShowCesModal( false ); - submitScore( recordedScore, comments ); + submitScore( { firstScore, secondScore, comments } ); }; return ( @@ -230,7 +244,15 @@ export const TaskListCompletedHeader: React.FC< { showCesModal ? ( { diff --git a/plugins/woocommerce/changelog/add-35124-ces-add-question b/plugins/woocommerce/changelog/add-35124-ces-add-question new file mode 100644 index 00000000000..550dc1d8ebe --- /dev/null +++ b/plugins/woocommerce/changelog/add-35124-ces-add-question @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Updating all CES events to support two questions in modal. diff --git a/plugins/woocommerce/src/Internal/Admin/CustomerEffortScoreTracks.php b/plugins/woocommerce/src/Internal/Admin/CustomerEffortScoreTracks.php index ab99e1a5c71..9ef4f2e65b0 100644 --- a/plugins/woocommerce/src/Internal/Admin/CustomerEffortScoreTracks.php +++ b/plugins/woocommerce/src/Internal/Admin/CustomerEffortScoreTracks.php @@ -142,11 +142,13 @@ class CustomerEffortScoreTracks { * an increase of the number of rows in tags table * * @param string $action Action name for the survey. - * @param string $label Label for the snackbar. + * @param string $title Title for the snackbar. + * @param string $first_question The text for the first question. + * @param string $second_question The text for the second question. * * @return string Generated JavaScript to append to page. */ - private function get_script_track_edit_php( $action, $label ) { + private function get_script_track_edit_php( $action, $title, $first_question, $second_question ) { return sprintf( "(function( $ ) { 'use strict'; @@ -158,7 +160,7 @@ class CustomerEffortScoreTracks { if ( $('.tags tbody > tr').length > initialCount ) { // New tag detected. clearInterval( interval ); - wp.data.dispatch('wc/customer-effort-score').addCesSurvey( '%s', '%s', window.pagenow, window.adminpage, '%s' ); + wp.data.dispatch('wc/customer-effort-score').addCesSurvey({ action: '%s', title: '%s', firstQuestion: '%s', secondQuestion: '%s', onsubmitLabel: '%s' }); } else { // Form is no longer loading, most likely failed. if ( $( '#addtag .submit .spinner.is-active' ).length < 1 ) { @@ -169,7 +171,9 @@ class CustomerEffortScoreTracks { }); })( jQuery );", esc_js( $action ), - esc_js( $label ), + esc_js( $title ), + esc_js( $first_question ), + esc_js( $second_question ), esc_js( $this->onsubmit_label ) ); } @@ -271,10 +275,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::SEARCH_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to use search?', 'woocommerce' ), + 'firstQuestion' => __( + 'The search feature in WooCommerce is easy to use.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'The search\'s functionality meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => $page_now, 'adminpage' => $admin_page, @@ -337,10 +349,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::PRODUCT_ADD_PUBLISH_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to add a product?', 'woocommerce' ), + 'firstQuestion' => __( + 'The product creation screen is easy to use.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'The product creation screen\'s functionality meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product', 'adminpage' => 'post-php', @@ -362,10 +382,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::PRODUCT_UPDATE_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to edit your product?', 'woocommerce' ), + 'firstQuestion' => __( + 'The product update process is easy to complete.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'The product update process meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product', 'adminpage' => 'post-php', @@ -387,10 +415,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::SHOP_ORDER_UPDATE_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to update an order?', 'woocommerce' ), + 'firstQuestion' => __( + 'The order details screen is easy to use.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'The order details screen\'s functionality meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'shop_order', 'adminpage' => 'post-php', @@ -447,7 +483,9 @@ class CustomerEffortScoreTracks { wc_enqueue_js( $this->get_script_track_edit_php( self::ADD_PRODUCT_CATEGORIES_ACTION_NAME, - __( 'How easy was it to add product category?', 'woocommerce' ) + __( 'How easy was it to add product category?', 'woocommerce' ), + __( 'The product category details screen is easy to use.', 'woocommerce' ), + __( "The product category details screen's functionality meets my needs.", 'woocommerce' ) ) ); } @@ -463,7 +501,9 @@ class CustomerEffortScoreTracks { wc_enqueue_js( $this->get_script_track_edit_php( self::ADD_PRODUCT_TAGS_ACTION_NAME, - __( 'How easy was it to add a product tag?', 'woocommerce' ) + __( 'How easy was it to add a product tag?', 'woocommerce' ), + __( 'The product tag details screen is easy to use.', 'woocommerce' ), + __( "The product tag details screen's functionality meets my needs.", 'woocommerce' ) ) ); } @@ -484,10 +524,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::IMPORT_PRODUCTS_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to import products?', 'woocommerce' ), + 'firstQuestion' => __( + 'The product import process is easy to complete.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'The product import process meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product_page_product_importer', 'adminpage' => 'product_page_product_importer', @@ -519,10 +567,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::SETTINGS_CHANGE_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to update your settings?', 'woocommerce' ), + 'firstQuestion' => __( + 'The settings screen is easy to use.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'The settings screen\'s functionality meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'woocommerce_page_wc-settings', 'adminpage' => 'woocommerce_page_wc-settings', @@ -542,10 +598,18 @@ class CustomerEffortScoreTracks { $this->enqueue_to_ces_tracks( array( 'action' => self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME, - 'label' => __( + 'title' => __( 'How easy was it to add a product attribute?', 'woocommerce' ), + 'firstQuestion' => __( + 'Product attributes are easy to use.', + 'woocommerce' + ), + 'secondQuestion' => __( + 'Product attributes\' functionality meets my needs.', + 'woocommerce' + ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product_page_product_attributes', 'adminpage' => 'product_page_product_attributes',