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

Consolidate accounts onboarding: Automatically preselect a Google Ads account when there is only one, as well as adjust the UI presentation #2608

Merged
53 changes: 53 additions & 0 deletions js/src/components/ads-account-select-control/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import AppSelectControl from '.~/components/app-select-control';
import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts';

/**
* @param {Object} props The component props
* @param {string} [props.value] The selected value. IF no value is defined, then the first option is selected and onChange function is triggered.
* @param {Function} [props.onChange] Callback when the select value changes.
* @return {JSX.Element} An enhanced AppSelectControl component.
*/
const AdsAccountSelectControl = ( {
value,
onChange = () => {},

Check warning on line 21 in js/src/components/ads-account-select-control/index.js

View check run for this annotation

Codecov / codecov/patch

js/src/components/ads-account-select-control/index.js#L21

Added line #L21 was not covered by tests
eason9487 marked this conversation as resolved.
Show resolved Hide resolved
...props
} ) => {
const { existingAccounts: accounts = [] } = useExistingGoogleAdsAccounts();
eason9487 marked this conversation as resolved.
Show resolved Hide resolved

const options = accounts.map( ( acc ) => ( {
eason9487 marked this conversation as resolved.
Show resolved Hide resolved
value: acc.id,
label: sprintf(
// translators: 1: account name, 2: account ID.
__( '%1$s (%2$s)', 'google-listings-and-ads' ),
acc.name,
acc.id
),
asvinb marked this conversation as resolved.
Show resolved Hide resolved
} ) );

useEffect( () => {
// Triggers the onChange event in order to pre-select the initial value
eason9487 marked this conversation as resolved.
Show resolved Hide resolved
if ( value === undefined ) {
asvinb marked this conversation as resolved.
Show resolved Hide resolved
onChange( options[ 0 ]?.value );
}
}, [ options, onChange, value ] );
asvinb marked this conversation as resolved.
Show resolved Hide resolved
eason9487 marked this conversation as resolved.
Show resolved Hide resolved

asvinb marked this conversation as resolved.
Show resolved Hide resolved
return (
<AppSelectControl
options={ options }
onChange={ onChange }
selectSingleValue
{ ...props }
/>
);
};

export default AdsAccountSelectControl;
28 changes: 24 additions & 4 deletions js/src/components/app-select-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,34 @@
* it will be added to the container div's `className`,
* so that you can further control its style.
*
* @param {*} props
* @param {*} props The component props.
* @param {boolean} [props.selectSingleValue=false] Whether the select should show only one value.
eason9487 marked this conversation as resolved.
Show resolved Hide resolved
*/
const AppSelectControl = ( props ) => {
const { className, ...rest } = props;
const { className, options, selectSingleValue = false, ...rest } = props;
const showSingleValue = selectSingleValue && options?.length === 1;

let selectProps = {
options,
...rest,
};

if ( showSingleValue ) {
selectProps = {

Check warning on line 32 in js/src/components/app-select-control/index.js

View check run for this annotation

Codecov / codecov/patch

js/src/components/app-select-control/index.js#L32

Added line #L32 was not covered by tests
...selectProps,
suffix: ' ',
tabIndex: '-1',
readOnly: true,
};
}

return (
<div className={ classNames( 'app-select-control', className ) }>
<SelectControl { ...rest } />
<div
className={ classNames( 'app-select-control', className, {
'app-select-control--has-single-value': showSingleValue,
asvinb marked this conversation as resolved.
Show resolved Hide resolved
} ) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to prefix the class names with gla or g4w

Copy link
Collaborator

@asvinb asvinb Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I see there are other parts where the app- prefix it used. For e.g most of the js/src/components/app-* components. Let's keep it as is.
Examples: https://github.com/search?q=repo%3Awoocommerce%2Fgoogle-listings-and-ads+.app-+language%3ASCSS&type=code&l=SCSS

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check this comment from Eason.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what you're saying @ankitrox , but if you look at the current codebase, there are components which have thegla-* prefix class names and other specific components com which have the app-* prefix. Clearly there is a reason to why this is so. In this particular case, we are editing an existing component which already uses the app-* prefix, so I would be reluctant to change it. If you look at

, you'll notice that it's using both, gla and app prefix. I think the base components used within the app are prefixed with app-*. @eason9487 Can you shed some light please, about why we have 2 different CSS prefix, i.e gla-* and app-*? Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early components often came with an app- prefix, but that is unnecessary and can be omitted for new components as it becomes a bit verbose.

Regarding the CSS class naming, the main reason is to reduce the chance of CSS naming collisions. app- has significantly higher generality, so gla- or g4w- would be more appropriate. The current app- uses are almostly carry-over from earlier times, and newer code would be better to avoid using it as a prefix.

If it involves changing existing code, it is not necessary to modify it. However, if the change (scope) is minor, it would be appreciated if it could be adjusted in the meantime.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification @eason9487 . I think that helps a lot. For the scope of this ticket, I suggest we keep it as is. We can have a separate ticket to adjust the class name. 👍

>
<SelectControl { ...selectProps } />
</div>
);
};
Expand Down
4 changes: 4 additions & 0 deletions js/src/components/app-select-control/index.scss
asvinb marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
margin-bottom: 0;
}
}

.app-select-control--has-single-value {
pointer-events: none;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import useApiFetchCallback from '.~/hooks/useApiFetchCallback';
import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
import useEventPropertiesFilter from '.~/hooks/useEventPropertiesFilter';
import AdsAccountSelectControl from './ads-account-select-control';
import AdsAccountSelectControl from '.~/components/ads-account-select-control';
import { useAppDispatch } from '.~/data';
import { FILTER_ONBOARDING } from '.~/utils/tracks';
import './index.scss';
Expand Down Expand Up @@ -95,7 +95,7 @@ const ConnectAds = ( props ) => {
<Section.Card.Body>
<Subsection.Title>
{ __(
'Select an existing account',
'Connect to an existing account',
'google-listings-and-ads'
) }
</Subsection.Title>
Expand All @@ -120,7 +120,6 @@ const ConnectAds = ( props ) => {
) }
<ContentButtonLayout>
<AdsAccountSelectControl
accounts={ accounts }
value={ value }
onChange={ setValue }
/>
Expand Down
30 changes: 14 additions & 16 deletions js/src/components/google-ads-account-card/connect-ads/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
import { useAppDispatch } from '.~/data';
import { FILTER_ONBOARDING } from '.~/utils/tracks';
import expectComponentToRecordEventWithFilteredProperties from '.~/tests/expectComponentToRecordEventWithFilteredProperties';
import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts';

jest.mock( '.~/hooks/useApiFetchCallback', () =>
jest.fn().mockName( 'useApiFetchCallback' )
Expand All @@ -24,6 +25,10 @@ jest.mock( '.~/hooks/useGoogleAdsAccount', () =>
jest.fn().mockName( 'useGoogleAdsAccount' )
);

jest.mock( '.~/hooks/useExistingGoogleAdsAccounts', () =>
jest.fn().mockName( 'useExistingGoogleAdsAccounts' )
);

jest.mock( '.~/data', () => ( {
...jest.requireActual( '.~/data' ),
useAppDispatch: jest.fn(),
Expand Down Expand Up @@ -64,6 +69,10 @@ describe( 'ConnectAds', () => {
.mockName( 'refetchGoogleAdsAccount' ),
} );

useExistingGoogleAdsAccounts.mockReturnValue( {
existingAccounts: accounts,
} );

fetchGoogleAdsAccountStatus = jest
.fn()
.mockName( 'fetchGoogleAdsAccountStatus' );
Expand All @@ -76,11 +85,7 @@ describe( 'ConnectAds', () => {

it( 'should render the given accounts in a selection', () => {
render( <ConnectAds accounts={ accounts } /> );

expect( screen.getByRole( 'combobox' ) ).toBeInTheDocument();
expect(
screen.getByRole( 'option', { name: 'Select one' } )
).toBeInTheDocument();
expect(
screen.getByRole( 'option', { name: 'Account A (1)' } )
).toBeInTheDocument();
Expand Down Expand Up @@ -115,22 +120,15 @@ describe( 'ConnectAds', () => {
expect( onCreateNew ).toHaveBeenCalledTimes( 1 );
} );

it( 'should disable the "Connect" button when no account is selected', async () => {
const user = userEvent.setup();
it( 'should disable the "Connect" button when there are no accounts', async () => {
useExistingGoogleAdsAccounts.mockReturnValue( {
existingAccounts: [],
} );

render( <ConnectAds accounts={ accounts } /> );
const combobox = screen.getByRole( 'combobox' );
render( <ConnectAds accounts={ [] } /> );
const button = getConnectButton();

expect( button ).toBeDisabled();

await user.selectOptions( combobox, '1' );

expect( button ).toBeEnabled();

await user.selectOptions( combobox, '' );

expect( button ).toBeDisabled();
} );

it( 'should render a connecting state and disable the button to switch to account creation after clicking the "Connect" button', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const ConnectMC = () => {
<Section.Card.Body>
<Subsection.Title>
{ __(
'Select an existing account',
'Connect to an existing account',
'google-listings-and-ads'
) }
</Subsection.Title>
Expand Down
1 change: 1 addition & 0 deletions js/src/components/merchant-center-select-control/index.js
eason9487 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const MerchantCenterSelectControl = ( {
options={ options }
onChange={ onChange }
value={ value }
selectSingleValue
{ ...props }
/>
);
Expand Down
8 changes: 0 additions & 8 deletions tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,18 +246,10 @@ test.describe( 'Set up Ads account', () => {
test( 'Select one existing account', async () => {
const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`;

await expect(
setupAdsAccounts.getConnectAdsButton()
).toBeDisabled();

await setupAdsAccounts.selectAnExistingAdsAccount(
adsAccountSelected
);

await expect(
setupAdsAccounts.getConnectAdsButton()
).toBeEnabled();

//Intercept Ads connection request
const connectAdsAccountRequest =
setupAdsAccounts.registerConnectAdsAccountRequests(
Expand Down
10 changes: 2 additions & 8 deletions tests/e2e/specs/setup-mc/step-1-accounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,12 +340,6 @@ test.describe( 'Set up accounts', () => {
await expect( select ).toBeVisible();
} );

test( 'should see connect button is disabled when no ads account is selected', async () => {
const button = setupAdsAccountPage.getConnectAdsButton();
await expect( button ).toBeVisible();
await expect( button ).toBeDisabled();
} );

test( 'should see connect button is enabled when an ads account is selected', async () => {
await setupAdsAccountPage.selectAnExistingAdsAccount(
'23456'
Expand Down Expand Up @@ -612,11 +606,11 @@ test.describe( 'Set up accounts', () => {
} );

test.describe( 'connect to an existing account', () => {
test( 'should see "Select an existing account" title', async () => {
test( 'should see "Connect to an existing account" title', async () => {
const selectAccountTitle =
setUpAccountsPage.getSelectExistingMCAccountTitle();
await expect( selectAccountTitle ).toContainText(
'Select an existing account'
'Connect to an existing account'
);
} );

Expand Down