Skip to content

Commit

Permalink
Show spotlight when switching to variable product type (#37413)
Browse files Browse the repository at this point in the history
* Show variable product tour
* Only show tour when product type is changed to variable
* Only show tour if it hasn't been shown before
* Add variable_product_tour_shown to UserPreferences type
* Store whether tour has been shown in user preferences
* Record Tracks events
* Add docblock for woocommerce_admin_get_user_data_fields filter
* Add test for tour
  • Loading branch information
mattsherman authored and Jon Lane committed Mar 29, 2023
1 parent aa33543 commit 4a4b1d8
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 10 deletions.
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add variable_product_tour_shown to UserPreferences type.
1 change: 1 addition & 0 deletions packages/js/data/src/user/types.ts
Expand Up @@ -25,6 +25,7 @@ export type UserPreferences = {
[ key: string ]: number;
};
taxes_report_columns?: string;
variable_product_tour_shown?: string;
variations_report_columns?: string;
};

Expand Down
@@ -0,0 +1,121 @@
/**
* External dependencies
*/
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { TourKit, TourKitTypes } from '@woocommerce/components';
import { useUserPreferences } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';

function getStepName(
steps: TourKitTypes.WooStep[],
currentStepIndex: number
) {
return steps[ currentStepIndex ]?.meta?.name;
}

export const VariableProductTour = () => {
const [ isTourOpen, setIsTourOpen ] = useState( false );

const { updateUserPreferences, variable_product_tour_shown: hasShownTour } =
useUserPreferences();

const config: TourKitTypes.WooConfig = {
steps: [
{
referenceElements: {
desktop: '.attribute_tab',
},
focusElement: {
desktop: '.attribute_tab',
},
meta: {
name: 'attributes',
heading: __( 'Start by adding attributes', 'woocommerce' ),
descriptions: {
desktop: __(
'Add attributes like size and color for customers to choose from on the product page. We will use them to generate product variations.',
'woocommerce'
),
},
primaryButton: {
text: __( 'Got it', 'woocommerce' ),
},
},
},
],
options: {
// WooTourKit does not handle merging of default options properly,
// so we need to duplicate the effects options here.
effects: {
spotlight: {
interactivity: {
enabled: true,
rootElementSelector: '#wpwrap',
},
},
arrowIndicator: true,
liveResize: {
mutation: true,
resize: true,
rootElementSelector: '#wpwrap',
},
},
},
closeHandler: ( steps, currentStepIndex ) => {
updateUserPreferences( { variable_product_tour_shown: 'yes' } );
setIsTourOpen( false );

if ( currentStepIndex === steps.length - 1 ) {
recordEvent( 'variable_product_tour_completed', {
step: getStepName(
steps as TourKitTypes.WooStep[],
currentStepIndex
),
} );
} else {
recordEvent( 'variable_product_tour_dismissed', {
step: getStepName(
steps as TourKitTypes.WooStep[],
currentStepIndex
),
} );
}
},
};

// show the tour when the product type is changed to variable
useEffect( () => {
const productTypeSelect = document.querySelector(
'#product-type'
) as HTMLSelectElement;

if ( hasShownTour === 'yes' || ! productTypeSelect ) {
return;
}

function handleProductTypeChange() {
if ( productTypeSelect.value === 'variable' ) {
setIsTourOpen( true );
recordEvent( 'variable_product_tour_started', {
step: getStepName( config.steps, 0 ),
} );
}
}

productTypeSelect.addEventListener( 'change', handleProductTypeChange );

return () => {
productTypeSelect.removeEventListener(
'change',
handleProductTypeChange
);
};
} );

if ( ! isTourOpen ) {
return null;
}

return <TourKit config={ config } />;
};
@@ -0,0 +1,13 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';

/**
* Internal dependencies
*/
import { VariableProductTour } from '../../guided-tours/variable-product-tour';

const root = document.createElement( 'div' );
root.setAttribute( 'id', 'variable-product-tour-root' );
render( <VariableProductTour />, document.body.appendChild( root ) );
1 change: 1 addition & 0 deletions plugins/woocommerce-admin/webpack.config.js
Expand Up @@ -65,6 +65,7 @@ const wpAdminScripts = [
'settings-tracking',
'order-tracking',
'product-import-tracking',
'variable-product-tour',
];
const getEntryPoints = () => {
const entryPoints = {
Expand Down
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

Show tour when product type is changed to variable.
12 changes: 12 additions & 0 deletions plugins/woocommerce/includes/admin/class-wc-admin-pointers.php
Expand Up @@ -38,6 +38,7 @@ public function setup_pointers_for_screen() {
switch ( $screen->id ) {
case 'product':
$this->create_product_tutorial();
$this->create_variable_product_tutorial();
break;
case 'woocommerce_page_wc-addons':
$this->create_wc_addons_tutorial();
Expand All @@ -64,6 +65,17 @@ public function create_product_tutorial() {
WCAdminAssets::register_script( 'wp-admin-scripts', 'product-tour', true );
}

/**
* Pointers for creating a variable product.
*/
public function create_variable_product_tutorial() {
if ( ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}

WCAdminAssets::register_script( 'wp-admin-scripts', 'variable-product-tour', true );
}

/**
* Pointers for accessing In-App Marketplace.
*/
Expand Down
8 changes: 7 additions & 1 deletion plugins/woocommerce/src/Internal/Admin/WCAdminUser.php
Expand Up @@ -103,7 +103,13 @@ public function update_user_data_values( $values, $user, $field_id ) {
* @return array Fields to expose over the WP user endpoint.
*/
public function get_user_data_fields() {
return apply_filters( 'woocommerce_admin_get_user_data_fields', array() );
/**
* Filter user data fields exposed over the WordPress user endpoint.
*
* @since 4.0.0
* @param array $fields Array of fields to expose over the WP user endpoint.
*/
return apply_filters( 'woocommerce_admin_get_user_data_fields', array( 'variable_product_tour_shown' ) );
}

/**
Expand Down
Expand Up @@ -40,6 +40,32 @@ test.describe( 'Add New Variable Product Page', () => {
await api.post( 'products/batch', { delete: ids } );
} );

test( 'shows the variable product tour', async ( { page } ) => {
await page.goto( 'wp-admin/post-new.php?post_type=product' );
await page.selectOption( '#product-type', 'variable', { force: true } );

// because of the way that the tour is dynamically positioned,
// Playwright can't automatically scroll the button into view,
// so we will manually scroll the attributes tab into view,
// which will cause the tour to be scrolled into view as well
await page
.locator( '.attribute_tab' )
.getByRole( 'link', { name: 'Attributes' } )
.scrollIntoViewIfNeeded();

// dismiss the variable product tour
await page
.getByRole( 'button', { name: 'Got it' } )
.click( { force: true } );

// wait for the tour's dismissal to be saved
await page.waitForResponse(
( response ) =>
response.url().includes( '/users/' ) &&
response.status() === 200
);
} );

test( 'can create product, attributes and variations, edit variations and delete variations', async ( {
page,
} ) => {
Expand All @@ -54,10 +80,18 @@ test.describe( 'Add New Variable Product Page', () => {
if ( i > 0 ) {
await page.click( 'button.add_attribute' );
}
await page.waitForSelector( `input[name="attribute_names[${ i }]"]` );
await page.waitForSelector(
`input[name="attribute_names[${ i }]"]`
);

await page.locator( `input[name="attribute_names[${ i }]"]` ).first().type( `attr #${ i + 1 }` );
await page.locator( `textarea[name="attribute_values[${ i }]"]` ).first().type( 'val1 | val2' );
await page
.locator( `input[name="attribute_names[${ i }]"]` )
.first()
.type( `attr #${ i + 1 }` );
await page
.locator( `textarea[name="attribute_values[${ i }]"]` )
.first()
.type( 'val1 | val2' );
}
await page.click( 'text=Save attributes' );
// wait for the attributes to be saved
Expand Down Expand Up @@ -167,10 +201,11 @@ test.describe( 'Add New Variable Product Page', () => {
} );
const variationsCount = await page.$$( '.woocommerce_variation' );
await expect( variationsCount ).toHaveLength( 0 );

} );

test( 'can manually add a variation, manage stock levels, set variation defaults and remove a variation', async ( { page } ) => {
test( 'can manually add a variation, manage stock levels, set variation defaults and remove a variation', async ( {
page,
} ) => {
await page.goto( 'wp-admin/post-new.php?post_type=product' );
await page.fill( '#title', manualVariableProduct );
await page.selectOption( '#product-type', 'variable', { force: true } );
Expand All @@ -180,10 +215,18 @@ test.describe( 'Add New Variable Product Page', () => {
if ( i > 0 ) {
await page.click( 'button.add_attribute' );
}
await page.waitForSelector( `input[name="attribute_names[${ i }]"]` );
await page.waitForSelector(
`input[name="attribute_names[${ i }]"]`
);

await page.locator( `input[name="attribute_names[${ i }]"]` ).first().type( `attr #${ i + 1 }` );
await page.locator( `textarea[name="attribute_values[${ i }]"]` ).first().type( 'val1 | val2' );
await page
.locator( `input[name="attribute_names[${ i }]"]` )
.first()
.type( `attr #${ i + 1 }` );
await page
.locator( `textarea[name="attribute_values[${ i }]"]` )
.first()
.type( 'val1 | val2' );
}
await page.click( 'text=Save attributes' );
// wait for the attributes to be saved
Expand Down Expand Up @@ -289,6 +332,8 @@ test.describe( 'Add New Variable Product Page', () => {
page.on( 'dialog', ( dialog ) => dialog.accept() );
await page.hover( '.woocommerce_variation' );
await page.click( '.remove_variation.delete' );
await expect( page.locator( '.woocommerce_variation' ) ).toHaveCount( 0 );
await expect( page.locator( '.woocommerce_variation' ) ).toHaveCount(
0
);
} );
} );

0 comments on commit 4a4b1d8

Please sign in to comment.