From 4547922f3f47391c299c87b47f4f720f251495cb Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Mon, 22 May 2023 11:21:16 +0800 Subject: [PATCH] Add core profiler user profile page (#38328) --- .../changelog/add-core-profiler-user-profile | 4 + .../src/experimental-select-control/README.md | 4 +- .../select-control.tsx | 8 +- .../js/internal-js-tests/src/setup-globals.js | 4 + .../components/choice/choice.scss | 74 ++++ .../components/choice/choice.tsx | 75 ++++ .../components/heading/style.scss | 32 +- .../multiple-selector/multiple-selector.scss | 113 ++++++ .../multiple-selector/multiple-selector.tsx | 97 +++++ .../multiple-selector/render-menu.tsx | 62 +++ .../client/core-profiler/index.tsx | 148 ++++++- .../core-profiler/pages/BusinessLocation.tsx | 5 +- .../core-profiler/pages/UserProfile.tsx | 304 +++++++++++++-- .../pages/tests/user-profile.test.tsx | 129 ++++++ .../client/core-profiler/style.scss | 195 ++++++---- .../core-profiler-machine.test.tsx.snap | 366 +++++++++++++++++- .../test/core-profiler-machine.test.tsx | 7 +- .../changelog/add-core-profiler-user-profile | 4 + 18 files changed, 1509 insertions(+), 122 deletions(-) create mode 100644 packages/js/components/changelog/add-core-profiler-user-profile create mode 100644 plugins/woocommerce-admin/client/core-profiler/components/choice/choice.scss create mode 100644 plugins/woocommerce-admin/client/core-profiler/components/choice/choice.tsx create mode 100644 plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.scss create mode 100644 plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.tsx create mode 100644 plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/render-menu.tsx create mode 100644 plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx create mode 100644 plugins/woocommerce/changelog/add-core-profiler-user-profile diff --git a/packages/js/components/changelog/add-core-profiler-user-profile b/packages/js/components/changelog/add-core-profiler-user-profile new file mode 100644 index 00000000000..ab57ba63f55 --- /dev/null +++ b/packages/js/components/changelog/add-core-profiler-user-profile @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add onKeyDown and readOnlyWhenClosed options to experimentalSelectControl diff --git a/packages/js/components/src/experimental-select-control/README.md b/packages/js/components/src/experimental-select-control/README.md index 2b33d9457e8..b02c562f378 100644 --- a/packages/js/components/src/experimental-select-control/README.md +++ b/packages/js/components/src/experimental-select-control/README.md @@ -105,4 +105,6 @@ Name | Type | Default | Description `onInputChange` | Function | `() => null` | A callback that fires when the user input has changed `onRemove` | Function | `() => null` | A callback that fires when a selected item has been removed `onSelect` | Function | `() => null` | A callback that fires when an item has been selected -`selected` | Array or Item | `undefined` | An array of selected items or a single selected item +`selected` | Array or Item | `undefined` | An array of selected items or a single selected item\ +`onKeyDown` | Function | `() => null` | A callback that fires when a key is pressed +`readOnlyWhenClosed` | Boolean | `false` | Whether the input should be read-only when the menu is closed diff --git a/packages/js/components/src/experimental-select-control/select-control.tsx b/packages/js/components/src/experimental-select-control/select-control.tsx index ccab1447611..0bb76218a6e 100644 --- a/packages/js/components/src/experimental-select-control/select-control.tsx +++ b/packages/js/components/src/experimental-select-control/select-control.tsx @@ -58,6 +58,7 @@ export type SelectControlProps< ItemType > = { ) => void; onRemove?: ( item: ItemType ) => void; onSelect?: ( selected: ItemType ) => void; + onKeyDown?: ( e: KeyboardEvent ) => void; onFocus?: ( data: { inputValue: string } ) => void; stateReducer?: ( state: UseComboboxState< ItemType | null >, @@ -70,6 +71,8 @@ export type SelectControlProps< ItemType > = { inputProps?: GetInputPropsOptions; suffix?: JSX.Element | null; showToggleButton?: boolean; + readOnlyWhenClosed?: boolean; + /** * This is a feature already implemented in downshift@7.0.0 through the * reducer. In order for us to use it this prop is added temporarily until @@ -118,6 +121,7 @@ function SelectControl< ItemType = DefaultItemType >( { onRemove = () => null, onSelect = () => null, onFocus = () => null, + onKeyDown = () => null, stateReducer = ( state, actionAndChanges ) => actionAndChanges.changes, placeholder, selected, @@ -126,6 +130,7 @@ function SelectControl< ItemType = DefaultItemType >( { inputProps = {}, suffix = , showToggleButton = false, + readOnlyWhenClosed = true, __experimentalOpenMenuOnFocus = false, }: SelectControlProps< ItemType > ) { const [ isFocused, setIsFocused ] = useState( false ); @@ -247,7 +252,7 @@ function SelectControl< ItemType = DefaultItemType >( { onRemove( item ); }; - const isReadOnly = ! isOpen && ! isFocused; + const isReadOnly = readOnlyWhenClosed && ! isOpen && ! isFocused; const selectedItemTags = multiple ? ( ( { setIsFocused( false ); } }, + onKeyDown, placeholder, disabled, ...inputProps, diff --git a/packages/js/internal-js-tests/src/setup-globals.js b/packages/js/internal-js-tests/src/setup-globals.js index 68686994952..e2b1469fa8a 100644 --- a/packages/js/internal-js-tests/src/setup-globals.js +++ b/packages/js/internal-js-tests/src/setup-globals.js @@ -5,6 +5,10 @@ import { setLocaleData } from '@wordpress/i18n'; import { registerStore } from '@wordpress/data'; import 'regenerator-runtime/runtime'; +// Mock the config module to avoid errors like: +// Core Error: Could not find config value for key ${ key }. Please make sure that if you need it then it has a default value assigned in config/_shared.json. +jest.mock( '@automattic/calypso-config' ); + // Due to the dependency @wordpress/compose which introduces the use of // ResizeObserver this global mock is required for some tests to work. global.ResizeObserver = require( 'resize-observer-polyfill' ); diff --git a/plugins/woocommerce-admin/client/core-profiler/components/choice/choice.scss b/plugins/woocommerce-admin/client/core-profiler/components/choice/choice.scss new file mode 100644 index 00000000000..d918a0beddf --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/choice/choice.scss @@ -0,0 +1,74 @@ +.woocommerce-profiler-choice-container { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: $gap-large $gap; + border: 1px solid $gray-200; + border-radius: 2px; + width: 100%; + cursor: pointer; + + &[data-selected] { + border: 2px solid var(--wp-admin-theme-color); + border-radius: 4px; + } + + &:focus-visible { + border: 2px solid var(--wp-admin-theme-color); + padding: $gap-large $gap; + } +} + +.woocommerce-profiler-choice { + display: flex; + flex-direction: row; + align-items: center; + + .woocommerce-profiler-choice-input { + opacity: 0; + position: absolute; + } + + label { + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: $gray-900; + display: flex; + position: relative; + } + + input + label::before { + content: ''; + width: 20px; + height: 20px; + border: 1px solid $gray-600; + border-radius: 16px; + margin-right: $gap; + display: inline-block; + } + + input[data-selected] + label::before { + border-color: var(--wp-admin-theme-color); + } + + input[data-selected] + label::after { + background-color: var(--wp-admin-theme-color); + align-self: center; + border-radius: 50%; + content: ''; + position: absolute; + width: 14px; + height: 14px; + left: 4px; + } +} + +.woocommerce-profiler-choice-sub-options { + margin-top: $gap-large; + width: 100%; + + > div { + width: 100%; + } +} diff --git a/plugins/woocommerce-admin/client/core-profiler/components/choice/choice.tsx b/plugins/woocommerce-admin/client/core-profiler/components/choice/choice.tsx new file mode 100644 index 00000000000..2d37bb7c131 --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/choice/choice.tsx @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; + +/** + * Internal dependencies + */ +import './choice.scss'; + +type Props = { + className?: string; + selected: boolean; + title: string; + name: string; + value: string; + onChange: ( value: string ) => void; + subOptionsComponent?: React.ReactNode; +}; + +export const Choice = ( { + className, + selected, + title, + name, + value, + onChange, + subOptionsComponent = null, +}: Props ) => { + const changeHandler = () => { + onChange( value ); + }; + const inputId = 'woocommerce-' + value.replace( /_/g, '-' ); + + return ( +
{ + if ( e.key === 'Enter' ) { + changeHandler(); + } + } } + data-selected={ selected ? selected : null } + tabIndex={ 0 } + > +
+ + +
+ { selected && subOptionsComponent && ( +
+ { subOptionsComponent } +
+ ) } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss b/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss index 1946fa3d6bc..c1cf617130f 100644 --- a/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss @@ -1,7 +1,11 @@ .woocommerce-profiler-heading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; margin-bottom: 48px; - width: 100%; max-width: 550px; + width: 100%; .woocommerce-profiler-heading__title { font-style: normal; @@ -31,4 +35,30 @@ color: $gray-800; } } + + &.woocommerce-profiler__stepper-heading { + margin-top: 72px; + + .woocommerce-profiler-heading__title { + margin-bottom: 0; + } + .woocommerce-profiler-heading__subtitle { + margin: 12px 0 0; + } + + @media (max-width: #{ ($break-mobile) }) { + margin-top: 52px 0 40px; + + .woocommerce-profiler-heading__title { + font-size: 32px; + line-height: 40px; + text-align: left; + } + .woocommerce-profiler-heading__subtitle { + text-align: left; + font-size: 16px; + color: $gray-700; + } + } + } } diff --git a/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.scss b/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.scss new file mode 100644 index 00000000000..1a3927cf485 --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.scss @@ -0,0 +1,113 @@ + +.woocommerce-experimental-select-control { + &:hover, + ul:hover, + .woocommerce-experimental-select-control__input:hover, + .components-popover:hover, + .woocommerce-experimental-select-control__combo-box-wrapper:hover { + cursor: pointer; + } + + &.has-selected-items { + .woocommerce-experimental-select-control__combox-box { + position: absolute; + width: 0; + z-index: -1; + } + } + + .woocommerce-experimental-select-control__combox-box .woocommerce-experimental-select-control__input { + font-size: 13px; + font-weight: 400; + line-height: 16px; + } + + .components-popover.woocommerce-experimental-select-control__popover-menu { + padding-bottom: 20px; + } + + .woocommerce-experimental-select-control__selected-item { + background: $gray-100; + padding: 2px 5px 2px 8px; + } + + .woocommerce-tag .woocommerce-tag__text { + background: none !important; + font-size: 12px; + line-height: 16px; + padding: 0 2px; + } + + .woocommerce-experimental-select-control__combo-box-wrapper { + transition: box-shadow 0.25s linear; + border: 1px solid #bbb; + border-radius: 2px; + } + + .woocommerce-experimental-select-control__popover-menu { + border: none; + left: 0 !important; + width: 100%; + } + + .woocommerce-experimental-select-control__popover-menu-container { + overflow-y: initial; + list-style: none; + padding: 0; + width: 100% !important; + } + + .components-popover { + transform: none !important; + + .components-popover__content { + transform: none; + width: 100%; + border: 1px solid #ccc; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + border-radius: 2px; + overflow-x: hidden !important; + + ul li { + &:hover { + background-color: #eff2fd !important; + cursor: pointer; + } + + .components-base-control__field { + margin-bottom: 0 !important; + } + + .components-checkbox-control__label { + cursor: pointer; + font-size: 13px; + font-weight: normal !important; + pointer-events: none; + } + + .components-checkbox-control__input-container { + border: 1px solid #757575; + border-radius: 2px; + height: 20px !important; + padding: 1px; + width: 20px !important; + + .components-checkbox-control__input[type='checkbox'] { + border: none; + height: 16px !important; + width: 16px !important; + &:checked { + border-radius: 2px; + } + } + + svg { + height: 20px; + top: -1px; + width: 22px; + } + } + } + } + } +} diff --git a/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.tsx b/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.tsx new file mode 100644 index 00000000000..460621d4922 --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/multiple-selector.tsx @@ -0,0 +1,97 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + __experimentalSelectControl as SelectControl, + selectControlStateChangeTypes, +} from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { renderMenu } from './render-menu'; +import './multiple-selector.scss'; + +type Props = { + options: Array< { label: string; value: string } >; + onSelect: ( + selectedOptions: Array< { label: string; value: string } > + ) => void; + selectedOptions?: Array< { label: string; value: string } >; + placeholder?: string; + onOpenClose?: ( isOpen: boolean ) => void; +}; + +export const MultipleSelector = ( { + options, + onSelect, + selectedOptions = [], + placeholder = __( 'Select platforms', 'woocommerce' ), + onOpenClose = () => {}, +}: Props ) => { + return ( + allItems } + selected={ selectedOptions } + inputProps={ { + 'aria-readonly': true, + 'aria-label': __( + 'Use up and down arrow keys to navigate', + 'woocommerce' + ), + } } + onKeyDown={ ( e ) => { + if ( e.key.length <= 1 ) { + e.preventDefault(); + return false; + } + } } + placeholder={ selectedOptions.length ? '' : placeholder } + stateReducer={ ( state, actionAndChanges ) => { + const { changes, type } = actionAndChanges; + switch ( type ) { + case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem: + return { + ...changes, + inputValue: state.inputValue, + }; + case selectControlStateChangeTypes.ItemClick: + return { + ...changes, + isOpen: true, + inputValue: state.inputValue, + highlightedIndex: state.highlightedIndex, + }; + default: + return changes; + } + } } + onSelect={ ( item ) => { + if ( ! item ) { + return; + } + const exist = selectedOptions.find( + ( existingItem ) => existingItem.value === item.value + ); + const updatedPlatforms = exist + ? selectedOptions.filter( + ( existingItem ) => + existingItem.value !== item.value + ) + : [ ...selectedOptions, item ]; + onSelect( updatedPlatforms ); + } } + onRemove={ ( item ) => + onSelect( selectedOptions.filter( ( i ) => i !== item ) ) + } + > + { renderMenu( { selectedOptions, onOpenClose } ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/render-menu.tsx b/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/render-menu.tsx new file mode 100644 index 00000000000..4eb21be2f27 --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/multiple-selector/render-menu.tsx @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { CheckboxControl } from '@wordpress/components'; +import { useEffect } from '@wordpress/element'; +import { + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; +import { ChildrenProps } from '@woocommerce/components/build-types/experimental-select-control/types'; + +type Props = { + selectedOptions: Array< { label: string; value: string } >; + onOpenClose: ( isOpen: boolean ) => void; +}; + +export const renderMenu = + ( { selectedOptions, onOpenClose }: Props ) => + ( { + items, + highlightedIndex, + isOpen, + getItemProps, + getMenuProps, + }: ChildrenProps< { + label: string; + value: string; + } > ) => { + useEffect( () => { + onOpenClose( isOpen ); + }, [ isOpen ] ); + + return ( + + { items.map( ( item, menuIndex ) => { + const isSelected = selectedOptions.includes( item ); + return ( + + {} } + checked={ isSelected } + label={ item.label } + /> + + ); + } ) } + + ); + }; diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 0e05136786f..ecf48f28f24 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -22,7 +22,12 @@ import { initializeExPlat } from '@woocommerce/explat'; * Internal dependencies */ import { IntroOptIn } from './pages/IntroOptIn'; -import { UserProfile } from './pages/UserProfile'; +import { + UserProfile, + BusinessChoice, + SellingOnlineAnswer, + SellingPlatform, +} from './pages/UserProfile'; import { BusinessInfo } from './pages/BusinessInfo'; import { BusinessLocation } from './pages/BusinessLocation'; import { getCountryStateOptions } from './services/country'; @@ -76,9 +81,22 @@ export type ExtensionsEvent = { }; }; +// TODO: add types as we develop the pages +export type OnboardingProfile = { + business_choice: BusinessChoice; + selling_online_answer: SellingOnlineAnswer | null; + selling_platforms: SellingPlatform[] | null; + skip?: boolean; +}; + export type CoreProfilerStateMachineContext = { optInDataSharing: boolean; - userProfile: { foo: { bar: 'qux' }; skipped: false } | { skipped: true }; + userProfile: { + businessChoice?: BusinessChoice; + sellingOnlineAnswer?: SellingOnlineAnswer | null; + sellingPlatforms?: SellingPlatform[] | null; + skipped?: boolean; + }; geolocatedLocation: { location: string; }; @@ -126,6 +144,36 @@ const handleCountries = assign( { }, } ); +const getOnboardingProfileOption = async () => + resolveSelect( OPTIONS_STORE_NAME ).getOption( + 'woocommerce_onboarding_profile' + ); + +const handleOnboardingProfileOption = assign( { + userProfile: ( + _context, + event: DoneInvokeEvent< OnboardingProfile | undefined > + ) => { + if ( ! event.data ) { + return {}; + } + + const { + business_choice: businessChoice, + selling_online_answer: sellingOnlineAnswer, + selling_platforms: sellingPlatforms, + ...rest + } = event.data; + + return { + ...rest, + businessChoice, + sellingOnlineAnswer, + sellingPlatforms, + }; + }, +} ); + const redirectToWooHome = () => { navigateTo( { url: getNewPath( {}, '/', {} ) } ); }; @@ -148,6 +196,35 @@ const recordTracksIntroViewed = () => { } ); }; +const recordTracksUserProfileViewed = () => { + recordEvent( 'storeprofiler_step_view', { + step: 'user_profile', + wc_version: getSetting( 'wcVersion' ), + } ); +}; + +const recordTracksUserProfileCompleted = ( + _context: CoreProfilerStateMachineContext, + event: Extract< UserProfileEvent, { type: 'USER_PROFILE_COMPLETED' } > +) => { + recordEvent( 'storeprofiler_step_complete', { + step: 'user_profile', + wc_version: getSetting( 'wcVersion' ), + } ); + + recordEvent( 'storeprofiler_user_profile', { + business_choice: event.payload.userProfile.businessChoice, + selling_online_answer: event.payload.userProfile.sellingOnlineAnswer, + selling_platforms: event.payload.userProfile.sellingPlatforms + ? event.payload.userProfile.sellingPlatforms.join() + : null, + } ); +}; + +const recordTracksUserProfileSkipped = () => { + recordEvent( 'storeprofiler_user_profile_skip' ); +}; + const recordTracksSkipBusinessLocationViewed = () => { recordEvent( 'storeprofiler_step_view', { step: 'skip_business_location', @@ -183,6 +260,22 @@ const updateTrackingOption = ( } ); }; +const updateOnboardingProfileOption = ( + context: CoreProfilerStateMachineContext +) => { + const { businessChoice, sellingOnlineAnswer, sellingPlatforms, ...rest } = + context.userProfile; + + return dispatch( OPTIONS_STORE_NAME ).updateOptions( { + woocommerce_onboarding_profile: { + ...rest, + business_choice: businessChoice, + selling_online_answer: sellingOnlineAnswer, + selling_platforms: sellingPlatforms, + }, + } ); +}; + const updateBusinessLocation = ( countryAndState: string ) => { return dispatch( OPTIONS_STORE_NAME ).updateOptions( { woocommerce_default_country: countryAndState, @@ -238,10 +331,14 @@ const coreProfilerMachineActions = { recordTracksIntroCompleted, recordTracksIntroSkipped, recordTracksIntroViewed, + recordTracksUserProfileCompleted, + recordTracksUserProfileSkipped, + recordTracksUserProfileViewed, recordTracksSkipBusinessLocationViewed, recordTracksSkipBusinessLocationCompleted, assignOptInDataSharing, handleCountries, + handleOnboardingProfileOption, redirectToWooHome, }; @@ -249,6 +346,7 @@ const coreProfilerMachineServices = { getAllowTrackingOption, getCountries, getExtensions, + getOnboardingProfileOption, }; export const coreProfilerStateMachineDefinition = createMachine( { id: 'coreProfiler', @@ -296,7 +394,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { introOptIn: { on: { INTRO_COMPLETED: { - target: 'userProfile', + target: 'preUserProfile', actions: [ 'assignOptInDataSharing', 'updateTrackingOption', @@ -328,10 +426,25 @@ export const coreProfilerStateMachineDefinition = createMachine( { component: IntroOptIn, }, }, + preUserProfile: { + invoke: { + src: 'getOnboardingProfileOption', + onDone: [ + { + actions: [ 'handleOnboardingProfileOption' ], + target: 'userProfile', + }, + ], + onError: { + target: 'userProfile', + }, + }, + }, userProfile: { + entry: [ 'recordTracksUserProfileViewed' ], on: { USER_PROFILE_COMPLETED: { - target: 'preBusinessInfo', + target: 'postUserProfile', actions: [ assign( { userProfile: ( context, event: UserProfileEvent ) => @@ -340,7 +453,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { ], }, USER_PROFILE_SKIPPED: { - target: 'preBusinessInfo', + target: 'postUserProfile', actions: [ assign( { userProfile: ( context, event: UserProfileEvent ) => @@ -349,11 +462,36 @@ export const coreProfilerStateMachineDefinition = createMachine( { ], }, }, + exit: actions.choose( [ + { + cond: ( _context, event ) => + event.type === 'USER_PROFILE_COMPLETED', + actions: 'recordTracksUserProfileCompleted', + }, + { + cond: ( _context, event ) => + event.type === 'USER_PROFILE_SKIPPED', + actions: 'recordTracksUserProfileSkipped', + }, + ] ), meta: { progress: 40, component: UserProfile, }, }, + postUserProfile: { + invoke: { + src: ( context ) => { + return updateOnboardingProfileOption( context ); + }, + onDone: { + target: 'preBusinessInfo', + }, + onError: { + target: 'preBusinessInfo', + }, + }, + }, preBusinessInfo: { always: [ // immediately transition to businessInfo without any events as long as geolocation parallel has completed diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx index 69531956c54..3c5fcf2bdbe 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx @@ -41,6 +41,7 @@ export const BusinessLocation = ( {
-
+
- - + +
+
+ + { __( + 'Which one of these best describes you?', + 'woocommerce' + ) } + + { businessOptions.map( ( { title, value } ) => { + return ( + { + setBusinessChoice( + _value as BusinessChoice + ); + } } + subOptionsComponent={ + value === 'im_already_selling' + ? renderAlreadySellingOptions() + : null + } + /> + ); + } ) } +
+
+
+ +
+
+
); }; diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx new file mode 100644 index 00000000000..6c60b07a270 --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx @@ -0,0 +1,129 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +import { render, screen, fireEvent } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { UserProfile } from '../UserProfile'; +import { CoreProfilerStateMachineContext } from '../..'; + +describe( 'UserProfile', () => { + let props: { + sendEvent: jest.Mock; + navigationProgress: number; + context: Pick< CoreProfilerStateMachineContext, 'userProfile' >; + }; + + beforeEach( () => { + props = { + sendEvent: jest.fn(), + navigationProgress: 0, + context: { + userProfile: { + businessChoice: 'im_just_starting_my_business', + sellingOnlineAnswer: null, + sellingPlatforms: null, + }, + }, + }; + } ); + + it( 'should render user profile page', () => { + // @ts-ignore + render( ); + expect( + screen.getByText( /Which one of these best describes you?/i, { + selector: 'h1', + } ) + ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { + name: /Continue/i, + } ) + ).toBeInTheDocument(); + } ); + + it( 'should show online selling question when choosing "im_already_selling"', () => { + // @ts-ignore + render( ); + const radioInput = screen.getByLabelText< HTMLInputElement >( + "I'm already selling" + ); // Replace with the label of your radio button + + // Perform the radio button selection + fireEvent.click( radioInput ); + + // Assert the expected behavior + expect( radioInput.checked ).toBe( true ); + + const onlineSellingQuestion = screen.getByText( + /Are you selling online?/i + ); + expect( onlineSellingQuestion ).toBeInTheDocument(); + } ); + + it( 'should show online selling question when choosing "Yes, I\'m selling online"', () => { + render( + // @ts-ignore + + ); + const platformSelector = screen.getByLabelText( /Select an option/i ); + expect( platformSelector ).toBeInTheDocument(); + } ); + + it( 'should call sendEvent with USER_PROFILE_COMPLETED event when button is clicked', () => { + render( + // @ts-ignore + + ); + screen + .getByRole( 'button', { + name: /Continue/i, + } ) + .click(); + expect( props.sendEvent ).toHaveBeenCalledWith( { + type: 'USER_PROFILE_COMPLETED', + payload: { + userProfile: { + businessChoice: 'im_just_starting_my_business', + sellingOnlineAnswer: null, + sellingPlatforms: null, + }, + }, + } ); + } ); + + it( 'should call sendEvent with USER_PROFILE_SKIPPED event when skip button is clicked', () => { + render( + // @ts-ignore + + ); + screen + .getByRole( 'button', { + name: /Skip this setup/i, + } ) + .click(); + expect( props.sendEvent ).toHaveBeenCalledWith( { + type: 'USER_PROFILE_SKIPPED', + payload: { + userProfile: { + skipped: true, + }, + }, + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/core-profiler/style.scss b/plugins/woocommerce-admin/client/core-profiler/style.scss index 0776b7f9406..ce77508a119 100644 --- a/plugins/woocommerce-admin/client/core-profiler/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/style.scss @@ -19,6 +19,85 @@ @include breakpoint( '<782px' ) { padding: 0 20px; } + + .woocommerce-profiler-button-container { + width: 100%; + max-width: 404px; + @include breakpoint( '<782px' ) { + position: absolute; + bottom: 20px; + padding: 0 20px; + } + } + + .woocommerce-profiler-button { + display: flex; + width: 100%; + justify-content: center; + padding: 10px 16px; + height: 48px; + font-size: 14px; + font-weight: 500; + } + + .woocommerce-select-control__option { + font-size: 13px; + height: 40px; + min-height: initial; + + &:hover { + background: #eff2fd; + color: $gray-900; + } + } + .woocommerce-select-control__listbox { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #ccc; + margin-top: 8px; + top: 40px; + } + + .woocommerce-select-control__control { + height: 40px; + padding: 12px; + border: 1px solid #bbb; + + label, + .woocommerce-select-control__control-input { + font-size: 13px; + cursor: pointer; + } + + .components-base-control__label { + font-size: 13px; + line-height: 16px; + color: $gray-700; + } + + &.with-value { + .woocommerce-select-control__control-input { + margin: 0; + color: $gray-900; + } + + .components-base-control__label { + display: none; + } + } + + &.is-active { + border: 1px solid var(--wp-admin-theme-color); + } + } + + #woocommerce-select-control__listbox-0 { + top: 40px !important; + } +} + +.woocommerce-profiler-select-control__country { + max-width: 404px; + margin: auto auto 32px auto; } // Intro opt-in page @@ -117,6 +196,11 @@ max-width: 514px; margin-left: 10px; } + + .woocommerce-select-control__control-input { + font-size: 13px; + line-height: 16px; + } } a { @@ -124,38 +208,8 @@ } } } -.woocommerce-profiler-select-control__country { - max-width: 400px; - margin: auto auto 32px auto; - .woocommerce-select-control__option { - font-size: 13px; - &:hover { - background: #eff2fd; - } - } - .woocommerce-select-control__listbox { - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); - border: 1px solid #ccc; - margin-top: 8px; - } - .woocommerce-select-control__control { - height: 40px; - padding: 12px; - border: 1px solid #bbb; - label, - input { - font-size: 13px; - color: var(--wp-components-color-foreground, #757575); - } - &.is-active { - border: 1px solid #3858e9; - } - } - #woocommerce-select-control__listbox-0 { - top: 40px !important; - } -} +// Business location page .woocommerce-profiler-business-location { display: flex; flex-direction: column; @@ -169,42 +223,6 @@ width: 100%; } - .woocommerce-profiler-heading { - margin-top: 72px; - @include breakpoint( '<782px' ) { - margin-top: 52px; - margin-bottom: 40px; - h1 { - font-size: 32px; - line-height: 40px; - text-align: left; - } - h2 { - text-align: left; - font-size: 16px; - margin-bottom: 0; - } - } - } - .woocommerce-profiler-go-to-mystore__button-container { - width: 100%; - max-width: 400px; - @include breakpoint( '<782px' ) { - position: absolute; - bottom: 20px; - padding: 0 20px; - } - } - - .woocommerce-profiler-go-to-mystore__button { - display: flex; - width: 100%; - justify-content: center; - padding: 10px 16px; - height: 48px; - font-size: 14px; - font-weight: 500; - } .components-base-control__field { height: 40px; } @@ -232,6 +250,7 @@ } } +// Loader page .woocommerce-profiler-loader { width: 100%; height: 100%; @@ -283,3 +302,45 @@ } } } + +// User profile page +.woocommerce-profiler-user-profile { + .woocommerce-profiler-user-profile__content { + min-height: 630px; + + &.is-platform-selector-open { + padding-bottom: 145px; + } + } + + .woocommerce-user-profile-choices { + margin-bottom: 32px; + max-width: 404px; + width: 100%; + } + + .woocommerce-profiler-heading__title { + max-width: 480px; + } + + .woocommerce-user-profile-choices fieldset { + display: flex; + gap: 18px; + flex-direction: column; + } + + .woocommerce-profiler-choice-sub-options { + .woocommerce-profiler-question-label { + text-transform: uppercase; + color: $gray-900; + font-weight: 500; + font-size: 11px; + line-height: 16px; + margin: 0 0 8px; + } + } + + .woocommerce-profiler-selling-platform { + margin-top: 20px; + } +} diff --git a/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap b/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap index 11f06137a47..cb2aa9f7603 100644 --- a/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap +++ b/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap @@ -1955,7 +1955,7 @@ Object { class="woocommerce-profiler-page__content woocommerce-profiler-business-location__content" >

+
+
+ + - - , @@ -3051,16 +3214,179 @@ Object { class="woocommerce-profile-wizard__container woocommerce-profile-wizard__step-userProfile" > , "debug": [Function], diff --git a/plugins/woocommerce-admin/client/core-profiler/test/core-profiler-machine.test.tsx b/plugins/woocommerce-admin/client/core-profiler/test/core-profiler-machine.test.tsx index c6b573345de..d2bcbaa57ca 100644 --- a/plugins/woocommerce-admin/client/core-profiler/test/core-profiler-machine.test.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/test/core-profiler-machine.test.tsx @@ -14,14 +14,16 @@ import { createMachine } from 'xstate'; */ import { CoreProfilerController } from '../'; -jest.mock( '@automattic/calypso-config' ); - // mock out the external dependencies which we don't want to test here const actionOverrides = { updateTrackingOption: jest.fn(), + updateOnboardingProfileOption: jest.fn(), recordTracksIntroCompleted: jest.fn(), recordTracksIntroSkipped: jest.fn(), recordTracksIntroViewed: jest.fn(), + recordTracksUserProfileCompleted: jest.fn(), + recordTracksUserProfileSkipped: jest.fn(), + recordTracksUserProfileViewed: jest.fn(), recordTracksSkipBusinessLocationViewed: jest.fn(), recordTracksSkipBusinessLocationCompleted: jest.fn(), redirectToWooHome: jest.fn(), @@ -34,6 +36,7 @@ const servicesOverrides = { .mockResolvedValue( [ { code: 'US', name: 'United States', states: [] }, ] ), + getOnboardingProfileOption: jest.fn().mockResolvedValue( {} ), }; /** diff --git a/plugins/woocommerce/changelog/add-core-profiler-user-profile b/plugins/woocommerce/changelog/add-core-profiler-user-profile new file mode 100644 index 00000000000..72b4a3e4bf8 --- /dev/null +++ b/plugins/woocommerce/changelog/add-core-profiler-user-profile @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add core profiler user profile page