Skip to content

Add PriceBenchmarkSuggestions component #2853

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

Merged
merged 40 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9179974
Add Badge component.
asvinb Apr 9, 2025
7e25ec5
Add EffectivenessIndicator component.
asvinb Apr 9, 2025
52b57c2
Add @wordpress/dataviews package.
asvinb Apr 9, 2025
ab6b5b2
Add basic styles and placeholder content.
asvinb Apr 9, 2025
727701e
Merge branch 'add/2826-price-benchmark-tab' into add/2828-price-sugge…
asvinb Apr 10, 2025
0136f0e
Merge branch 'feature/2824-price-benchmarks' into add/2828-price-sugg…
asvinb Apr 10, 2025
f2bfb16
Add badge colors.
asvinb Apr 10, 2025
8a18e67
Use new badge colors.
asvinb Apr 10, 2025
b74baf9
Tweak styles for table.
asvinb Apr 10, 2025
26a9ed0
Use CurrentFactory.formatAmount function.
asvinb Apr 10, 2025
7f36e34
Add Label component.
asvinb Apr 10, 2025
550c1eb
Add Price component.
asvinb Apr 10, 2025
c6020d5
Add PriceBenchmarkTable component.
asvinb Apr 10, 2025
0ac927d
Update suggestions table to use PriceBenchmarkTable component.
asvinb Apr 10, 2025
6963cad
Add placeholder ChangePrice component.
asvinb Apr 14, 2025
566aa89
Update datastore.
asvinb Apr 14, 2025
311e621
Merge branch 'feature/2824-price-benchmarks' into add/2828-price-sugg…
asvinb Apr 14, 2025
95bb6db
Add usePriceBenchmarkSuggestions hook.
asvinb Apr 14, 2025
201592f
Add correct way of getting effectiveness badge.
asvinb Apr 14, 2025
79edc72
Remove test data.
asvinb Apr 14, 2025
ff64912
Add @wordpress/viewport package.
asvinb Apr 14, 2025
be438a1
Add unspecified type for effectiveness.
asvinb Apr 15, 2025
1b29a27
Add E2E tests.
asvinb Apr 15, 2025
ce85471
Add unspecified effectiveness test.
asvinb Apr 15, 2025
d7073ea
Fix font sizes
asvinb Apr 15, 2025
a91842f
Add tests for mobile view.
asvinb Apr 15, 2025
21b1fac
Fix typo.
asvinb Apr 17, 2025
eb4f02a
Add check for non numbers for Price component.
asvinb Apr 17, 2025
333723a
Use same data format as the API.
asvinb Apr 17, 2025
6dd92ce
Exclude @wordpress/dataviews package from vendors chunk.
asvinb Apr 17, 2025
52b3bf8
Tweak webpack config.
asvinb Apr 17, 2025
e30112f
Temporarily increase bundle size.
asvinb Apr 17, 2025
83db740
Increase vendors bundle max size.
asvinb Apr 17, 2025
d865e18
Add useCallback to callback function.
asvinb Apr 17, 2025
a185509
Add unit tests for Price component.
asvinb Apr 18, 2025
e9d0b35
Change description to be string.
asvinb Apr 18, 2025
daf93bc
Fix searching and logic to show fields.
asvinb Apr 18, 2025
86a80fd
Cast description to string.
asvinb Apr 18, 2025
8904ea9
Enable sorting by product title.
asvinb Apr 18, 2025
c47df83
Remove dataview CSS overrides.
asvinb Apr 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .externalized.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["@woocommerce/block-templates","@woocommerce/components","@woocommerce/currency","@woocommerce/customer-effort-score","@woocommerce/data","@woocommerce/date","@woocommerce/navigation","@woocommerce/number","@woocommerce/product-editor","@woocommerce/settings","@woocommerce/tracks","@wordpress/api-fetch","@wordpress/components","@wordpress/compose","@wordpress/data","@wordpress/data-controls","@wordpress/date","@wordpress/dom","@wordpress/element","@wordpress/hooks","@wordpress/html-entities","@wordpress/i18n","@wordpress/primitives","@wordpress/url","jquery","lodash","react","react-dom"]
["@woocommerce/block-templates","@woocommerce/components","@woocommerce/currency","@woocommerce/customer-effort-score","@woocommerce/data","@woocommerce/date","@woocommerce/navigation","@woocommerce/number","@woocommerce/product-editor","@woocommerce/settings","@woocommerce/tracks","@wordpress/api-fetch","@wordpress/components","@wordpress/compose","@wordpress/data","@wordpress/data-controls","@wordpress/date","@wordpress/dom","@wordpress/element","@wordpress/hooks","@wordpress/html-entities","@wordpress/i18n","@wordpress/primitives","@wordpress/url","@wordpress/viewport","jquery","lodash","react","react-dom"]
40 changes: 40 additions & 0 deletions js/src/components/badge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import './index.scss';

/**
* Badge component.
*
* A reusable badge component that displays content with optional styling based on intent.
* This is a temporary port of the Badge component from the Gutenberg repository:
* https://github.com/WordPress/gutenberg/tree/trunk/packages/components/src/badge
* until that package is made public.
*
* @param {Object} props - The component props.
* @param {string} [props.className] - Additional CSS classes to apply to the badge.
* @param {string} [props.intent='default'] - The intent of the badge, which determines its styling.
* Possible values include 'default', 'success', 'info', 'warning ,'error', etc.
* @param {JSX.Element} props.children - The content to display inside the badge.
* @param {Object} [props.props] - Additional props to spread onto the badge element.
* @return {JSX.Element} The rendered badge component.
*/
function Badge( { className, intent = 'default', children, ...props } ) {
return (
<span
className={ classnames( 'gla-badge', className, {
[ `is-${ intent }` ]: intent,
} ) }
{ ...props }
>
<span className="gla-badge__content">{ children }</span>
</span>
);
}

export default Badge;
38 changes: 38 additions & 0 deletions js/src/components/badge/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.gla-badge {
padding: 0 $grid-unit-10;
min-height: $grid-unit-30;
max-width: 100%;
border-radius: $gla-border-radius;
font-size: $gla-font-smaller;
font-weight: 400;
line-height: $gla-line-height-smaller;
display: inline-flex;
align-items: center;
gap: 2px;

&:where(.is-default) {
background-color: $gray-100;
color: $gray-800;
}

&:where(.is-warning) {
background-color: $gla-color-yellow-0;
color: $gla-color-yellow-70;
}

&:where(.is-error) {
background-color: $gla-color-red-0;
color: $gla-color-red-70;
}

&:where(.is-success) {
background-color: $gla-color-green-0;
color: $gla-color-green-70;
}
}

.gla-badge__content {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
6 changes: 6 additions & 0 deletions js/src/css/abstracts/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@

$gla-color-green: #008a20;
$gla-color-red: #d94f4f;
$gla-color-green-0: #edfaef;
$gla-color-green-70: #005c12;
$gla-color-yellow-0: #fcf9e8;
$gla-color-yellow-70: #614200;
$gla-color-red-0: #fcf0f1;
$gla-color-red-70: #8a2424;
1 change: 1 addition & 0 deletions js/src/data/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const TYPES = {
RECEIVE_ADS_BUDGET_RECOMMENDATIONS: 'RECEIVE_ADS_BUDGET_RECOMMENDATIONS',
RECEIVE_GTIN_MIGRATION_STATUS: 'RECEIVE_GTIN_MIGRATION_STATUS',
RECEIVE_PRICE_BENCHMARK_SUMMARY: 'RECEIVE_PRICE_BENCHMARK_SUMMARY',
RECEIVE_PRICE_BENCHMARK_SUGGESTIONS: 'RECEIVE_PRICE_BENCHMARK_SUGGESTIONS',
};

export default TYPES;
26 changes: 25 additions & 1 deletion js/src/data/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@
export function* fetchPriceBenchmarkSummary() {
try {
const data = yield apiFetch( {
path: `${ API_NAMESPACE }/mc/price-benchmark/summary`,
path: `${ API_NAMESPACE }/mc/price-benchmarks/summary`,
} );

return {
Expand All @@ -1225,3 +1225,27 @@
);
}
}

/**
* Action to fetch the Price Benchmark suggestions.
*/
export function* fetchPriceBenchmarkSuggestions() {
try {
const data = yield apiFetch( {

Check warning on line 1234 in js/src/data/actions.js

View check run for this annotation

Codecov / codecov/patch

js/src/data/actions.js#L1232-L1234

Added lines #L1232 - L1234 were not covered by tests
path: `${ API_NAMESPACE }/mc/price-benchmarks`,
} );

return {

Check warning on line 1238 in js/src/data/actions.js

View check run for this annotation

Codecov / codecov/patch

js/src/data/actions.js#L1238

Added line #L1238 was not covered by tests
type: TYPES.RECEIVE_PRICE_BENCHMARK_SUGGESTIONS,
data,
};
} catch ( error ) {
handleApiError(

Check warning on line 1243 in js/src/data/actions.js

View check run for this annotation

Codecov / codecov/patch

js/src/data/actions.js#L1243

Added line #L1243 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

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

We'll probably need to add some handling here later for #2852.

error,
__(
'There was an error getting the price benchmark suggestions.',
'google-listings-and-ads'
)
);
}
}
5 changes: 5 additions & 0 deletions js/src/data/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,11 @@
return setIn( state, 'price_benchmark.summary', data );
}

case TYPES.RECEIVE_PRICE_BENCHMARK_SUGGESTIONS: {
const { data } = action;
return setIn( state, 'price_benchmark.suggestions', data );

Check warning on line 545 in js/src/data/reducer.js

View check run for this annotation

Codecov / codecov/patch

js/src/data/reducer.js#L543-L545

Added lines #L543 - L545 were not covered by tests
}

// Page will be reloaded after all accounts have been disconnected, so no need to mutate state.
case TYPES.DISCONNECT_ACCOUNTS_ALL:
default:
Expand Down
8 changes: 8 additions & 0 deletions js/src/data/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
fetchTargetAudience,
fetchMCSetup,
fetchPriceBenchmarkSummary,
fetchPriceBenchmarkSuggestions,
receiveGoogleAccountAccess,
receiveReport,
receiveMCProductStatistics,
Expand Down Expand Up @@ -591,3 +592,10 @@
export function* getPriceBenchmarkSummary() {
yield fetchPriceBenchmarkSummary();
}

/**
* Resolver for getting the Price Benchmark suggestions.
*/
export function* getPriceBenchmarkSuggestions() {
yield fetchPriceBenchmarkSuggestions();

Check warning on line 600 in js/src/data/resolvers.js

View check run for this annotation

Codecov / codecov/patch

js/src/data/resolvers.js#L599-L600

Added lines #L599 - L600 were not covered by tests
}
10 changes: 10 additions & 0 deletions js/src/data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,3 +415,13 @@
export const getPriceBenchmarkSummary = ( state ) => {
return state.price_benchmark.summary;
};

/**
* Retrieves the price benchmark suggestions from the state.
*
* @param {Object} state - The state object containing price benchmark data.
* @return {Array} The array of price benchmark suggestions.
*/
export const getPriceBenchmarkSuggestions = ( state ) => {
return state.price_benchmark.suggestions;

Check warning on line 426 in js/src/data/selectors.js

View check run for this annotation

Codecov / codecov/patch

js/src/data/selectors.js#L426

Added line #L426 was not covered by tests
};
27 changes: 27 additions & 0 deletions js/src/hooks/usePriceBenchmarkSuggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { STORE_KEY } from '~/data/constants';

const selectorName = 'getPriceBenchmarkSuggestions';

const usePriceBenchmarkSuggestions = () => {
return useSelect( ( select ) => {
const selector = select( STORE_KEY );

return {
suggestions: selector[ selectorName ](),
hasFinishedResolution: selector.hasFinishedResolution(
selectorName,
[]
),
};
}, [] );
};

export default usePriceBenchmarkSuggestions;
28 changes: 28 additions & 0 deletions js/src/pages/price-benchmark/change-price.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import AppButton from '~/components/app-button';

/**
* ChangePrice component.
*
* Placeholder component.
*
* @param {Object} props - Component properties.
* @param {number} props.productID - The ID of the product for which the price is being changed.
* @param {string} [props.label='Change price'] - The label text for the button. Defaults to 'Change price'.
* @return {JSX.Element} The rendered AppButton component.
*/
const ChangePrice = ( {
productID,
label = __( 'Change price', 'google-listings-and-ads' ),
} ) => {
return <AppButton id={ productID }>{ label }</AppButton>;
};

export default ChangePrice;
54 changes: 54 additions & 0 deletions js/src/pages/price-benchmark/constants.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';

export const TABLE_TYPE_SUGGESTIONS = 'suggestions';
export const TABLE_TYPE_ADJUSTMENTS = 'adjustments';

export const LABEL_PRICE_CHANGE_EFFECTIVENESS =
'LABEL_PRICE_CHANGE_EFFECTIVENESS';
export const LABEL_PRICE_ON_GOOGLE = 'LABEL_PRICE_ON_GOOGLE';
export const LABEL_PRICE_GAP = 'LABEL_PRICE_GAP';
export const LABEL_SUGGESTED_PRICE = 'LABEL_SUGGESTED_PRICE';
export const LABEL_REGULAR_PRICE = 'LABEL_REGULAR_PRICE';
export const LABEL_ACTION = 'LABEL_ACTION';
export const EFFECTIVENESS_UNSPECIFIED = 0;
export const EFFECTIVENESS_LOW = 1;
export const EFFECTIVENESS_MEDIUM = 2;
export const EFFECTIVENESS_HIGH = 3;

export const LABELS = {
[ LABEL_PRICE_CHANGE_EFFECTIVENESS ]: {
title: __( 'Change Effectiveness', 'google-listings-and-ads' ),
tooltip: __(
'Effectiveness tells you which products would benefit most from price changes. This rating takes into consideration the performance boost predicted by adjusting the sale price and the difference between your current price and the suggested price. Price suggestions with “High” effectiveness are predicted to drive the largest increase in performance. Keep in mind that predictions do not guarantee improvements in future performance.',
'google-listings-and-ads'
),
},
[ LABEL_PRICE_ON_GOOGLE ]: {
title: __( 'Avg. Price on Google', 'google-listings-and-ads' ),
tooltip: __(
'The effective price for a product across all retailers selling the same product weighted by customer clicks. Products are matched based on the GTIN you provide in the product details.',
'google-listings-and-ads'
),
},
[ LABEL_PRICE_GAP ]: {
title: __( 'Price Gap %', 'google-listings-and-ads' ),
tooltip: __(
'The percentage difference between your price and the price on Google for this product.',
'google-listings-and-ads'
),
},
[ LABEL_SUGGESTED_PRICE ]: {
title: __( 'Suggested Price', 'google-listings-and-ads' ),
tooltip: __(
'Suggested sale price predicted by Google for products that benefit most from pricing adjustments. It is based on advanced simulations at different price points over the past 7 days factoring in price elasticity, current performance and the performance impact on price changes for businesses similar to you. Use suggested sale prices as valuable directional guidance to help shape your pricing strategy. Learn more about how to change the sale price of your products. Keep in mind that predictions do not guarantee future performance outcomes.',
'google-listings-and-ads'
),
},
[ LABEL_REGULAR_PRICE ]: {
title: __( 'Regular Price', 'google-listings-and-ads' ),
},
[ LABEL_ACTION ]: {
title: __( 'Action', 'google-listings-and-ads' ),
},
};
55 changes: 55 additions & 0 deletions js/src/pages/price-benchmark/effectiveness-indicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import {
EFFECTIVENESS_UNSPECIFIED,
EFFECTIVENESS_LOW,
EFFECTIVENESS_MEDIUM,
EFFECTIVENESS_HIGH,
} from './constants';
import Badge from '~/components/badge';

const EFFECTIVENESS_MAP = {
[ EFFECTIVENESS_UNSPECIFIED ]: {
intent: 'default',
label: __( 'Unspecified', 'google-listings-and-ads' ),
},
[ EFFECTIVENESS_LOW ]: {
intent: 'error',
label: __( 'Low', 'google-listings-and-ads' ),
},
[ EFFECTIVENESS_MEDIUM ]: {
intent: 'warning',
label: __( 'Medium', 'google-listings-and-ads' ),
},
[ EFFECTIVENESS_HIGH ]: {
intent: 'success',
label: __( 'High', 'google-listings-and-ads' ),
},
};

/**
* Component to display an effectiveness indicator badge based on the provided effectiveness level.
*
* @param {Object} props - The component props.
* @param {string} props.effectiveness - The effectiveness level to determine the badge's intent and label.
* @return {JSX.Element|null} A Badge component with the corresponding intent and label, or null if the effectiveness level is invalid.
*/
const EffectivenessIndicator = ( { effectiveness } ) => {
if ( ! EFFECTIVENESS_MAP[ effectiveness ] ) {
return null;
}

return (
<Badge intent={ EFFECTIVENESS_MAP[ effectiveness ].intent }>
{ EFFECTIVENESS_MAP[ effectiveness ].label }
</Badge>
);
};

export default EffectivenessIndicator;
3 changes: 3 additions & 0 deletions js/src/pages/price-benchmark/index.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
@import "@wordpress/dataviews/build-style/style.css";

.gla-price-benchmark__card .app-tab-nav:first-child {
padding: 0 $grid-unit-40;
}

.gla-price-benchmark__comparison-chart {
margin: $grid-unit-40 0;
}

Loading