Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show spotlight when switching to variable product type #37413

Merged
merged 13 commits into from Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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 @@
/**
mattsherman marked this conversation as resolved.
Show resolved Hide resolved
* 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 } ) => {
mattsherman marked this conversation as resolved.
Show resolved Hide resolved
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
);
} );
} );