From 26eed8c367c67a33567eee1736e1cb53443254a1 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 11 Sep 2024 02:03:21 -0600 Subject: [PATCH] refactor(client/recommend): refactoring the recommend and instantiator --- docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md | 1 + docs/INTEGRATION_RECOMMENDATIONS.md | 4 +- .../src/Client/apis/Recommend.test.ts | 62 ++--- .../snap-client/src/Client/apis/Recommend.ts | 67 ++---- .../transforms/recommendationFiltersPost.ts | 4 +- packages/snap-client/src/types.ts | 25 +- .../components/Recommendations/Recs/Recs.tsx | 12 +- .../recommendation/recommendationBundle.cy.js | 2 +- .../RecommendationInstantiator.test.tsx | 87 ++++--- .../RecommendationInstantiator.tsx | 215 ++++++++---------- 10 files changed, 222 insertions(+), 257 deletions(-) diff --git a/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md b/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md index 51c9b8ec7..a546d171f 100644 --- a/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md +++ b/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md @@ -26,6 +26,7 @@ Context variables may be applied to individual recommendation profiles similar t | options.brands | array of brand strings | all | optional brand identifiers used in brand trending recommendation profiles | | options.branch | template branch overwrite | all | optional branch overwrite for recommendations template (advanced usage) | | options.filters | array of filters | all | optional recommendation filters | +| options.query | string | all | query to search | | options.realtime | boolean | all | optional update recommendations if cart contents change (requires [cart attribute tracking](https://github.com/searchspring/snap/blob/main/docs/INTEGRATION_TRACKING.md)) | | options.blockedItems | array of strings | all | SKU values to identify which products to exclude from the response | | options.batched | boolean (default: `true`)| all | only applies to recommendation context, optional disable profile from being batched in a single request, can also be set globally [via config](https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Recommendation) | diff --git a/docs/INTEGRATION_RECOMMENDATIONS.md b/docs/INTEGRATION_RECOMMENDATIONS.md index 5312157fa..b2ead6db2 100644 --- a/docs/INTEGRATION_RECOMMENDATIONS.md +++ b/docs/INTEGRATION_RECOMMENDATIONS.md @@ -33,6 +33,7 @@ Context variables are applied to individual recommendation profiles similar to h | Option | Value | Placement | Description | Required |---|---|:---:|---|:---:| | products | array of SKU strings | product detail page | SKU value(s) to identify the current product(s) being viewed | ✔️ | +| blockedItems | array of strings | all | SKU values to identify which products to exclude from the response | | | cart | array (or function that returns an array) of current cart skus | all | optional method of setting cart contents | | | shopper.id | logged in user unique identifier | all | required for personalization functionallity if not provided to the bundle (global) context | | @@ -47,10 +48,9 @@ Context variables are applied to individual recommendation profiles similar to h | options.brands | array of brand strings | all | optional brand identifiers used in brand trending recommendation profiles | | | options.branch | template branch overwrite | all | optional branch overwrite for recommendations template (advanced usage) | | | options.dedupe | boolean (default: `true`) | all | dedupe products across all profiles in the batch | | -| options.searchTerm | string | all | query to search | | +| options.query | string | dynamic custom | query to search | | | options.filters | array of filters | all | optional recommendation filters | | | options.realtime | boolean | all | optional update recommendations if cart contents change (requires [cart attribute tracking](https://github.com/searchspring/snap/blob/main/docs/INTEGRATION_TRACKING.md)) | | -| options.blockedItems | array of strings | all | SKU values to identify which products to exclude from the response | | | options.limit | number (default: 20, max: 20) | all | optional maximum number of results to display, can also be set globally [via config globals](https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Recommendation) | | diff --git a/packages/snap-client/src/Client/apis/Recommend.test.ts b/packages/snap-client/src/Client/apis/Recommend.test.ts index 1d382253f..acbd9b476 100644 --- a/packages/snap-client/src/Client/apis/Recommend.test.ts +++ b/packages/snap-client/src/Client/apis/Recommend.test.ts @@ -3,7 +3,7 @@ import { ApiConfiguration } from './Abstract'; import { RecommendAPI } from './Recommend'; import { MockData } from '@searchspring/snap-shared'; -import type { PostRecommendAPISpec } from '../../types'; +import type { RecommendPostRequestModel } from '../../types'; const mockData = new MockData(); @@ -63,7 +63,7 @@ describe('Recommend Api', () => { }, }; - const requestParameters: PostRecommendAPISpec = { + const requestParameters: RecommendPostRequestModel = { siteId: '8uyt2m', profiles: [ { @@ -147,14 +147,14 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'similar', - limits: 14, + limit: 14, batched: true, ...batchParams, }); api.batchRecommendations({ tag: 'crossSell', - limits: 10, + limit: 10, batched: true, ...batchParams, }); @@ -167,7 +167,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"similar","limit":14},{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"similar","limit":14},{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -185,14 +185,14 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'similar', categories: ['shirts'], - limits: 14, + limit: 14, batched: true, ...batchParams, }); //no category api.batchRecommendations({ tag: 'crossSell', - limits: 10, + limit: 10, batched: true, ...batchParams, }); @@ -200,7 +200,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'crossSell', categories: ['pants'], - limits: 10, + limit: 10, batched: true, ...batchParams, }); @@ -213,7 +213,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"similar","categories":["shirts"],"limit":14},{"tag":"crossSell","limit":10},{"tag":"crossSell","categories":["pants"],"limit":10}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"similar","categories":["shirts"],"limit":14},{"tag":"crossSell","limit":10},{"tag":"crossSell","categories":["pants"],"limit":10}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -231,7 +231,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'similar', brands: ['shirts'], - limits: 14, + limit: 14, batched: true, ...batchParams, }); @@ -239,7 +239,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'crossSell', brands: ['pants', 'pants2'], - limits: 10, + limit: 10, batched: true, ...batchParams, }); @@ -252,7 +252,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"similar","brands":["shirts"],"limit":14},{"tag":"crossSell","brands":["pants","pants2"],"limit":10}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"similar","brands":["shirts"],"limit":14},{"tag":"crossSell","brands":["pants","pants2"],"limit":10}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -270,7 +270,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'similar', categories: ['shirts'], - limits: 14, + limit: 14, order: 3, batched: true, ...batchParams, @@ -278,14 +278,14 @@ describe('Recommend Api', () => { //no order api.batchRecommendations({ tag: 'crossSell', - limits: 10, + limit: 10, batched: true, ...batchParams, }); //no category api.batchRecommendations({ tag: 'crossSell', - limits: 10, + limit: 10, order: 2, batched: true, ...batchParams, @@ -294,7 +294,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'crossSell', categories: ['pants'], - limits: 10, + limit: 10, order: 1, batched: true, ...batchParams, @@ -305,7 +305,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","categories":["pants"],"limit":10},{"tag":"crossSell","limit":10},{"tag":"similar","categories":["shirts"],"limit":14},{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crossSell","categories":["pants"],"limit":10},{"tag":"crossSell","limit":10},{"tag":"similar","categories":["shirts"],"limit":14},{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; //add delay for paramBatch.timeout @@ -326,7 +326,7 @@ describe('Recommend Api', () => { const promise1 = api.batchRecommendations({ tag: 'similar', categories: ['shirts'], - limits: 10, + limit: 10, order: 2, batched: true, ...batchParams, @@ -335,7 +335,7 @@ describe('Recommend Api', () => { const promise2 = api.batchRecommendations({ tag: 'crosssell', categories: ['dress'], - limits: 20, + limit: 20, order: 1, batched: true, ...batchParams, @@ -349,7 +349,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crosssell","categories":["dress"],"limit":20},{"tag":"similar","categories":["shirts"],"limit":10}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crosssell","categories":["dress"],"limit":20},{"tag":"similar","categories":["shirts"],"limit":10}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -371,7 +371,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'crossSell', - limits: 10, + limit: 10, filters: [ { type: 'value', @@ -389,7 +389,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"],"filters":[{"field":"color","type":"=","values":["red"]}]}', + body: '{"profiles":[{"tag":"crossSell","limit":10,"filters":[{"field":"color","type":"=","values":["red"]}]}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -407,7 +407,7 @@ describe('Recommend Api', () => { api.batchRecommendations({ tag: 'crossSell', ...batchParams, - limits: undefined, + limit: undefined, }); //add delay for paramBatch.timeout @@ -417,7 +417,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -465,7 +465,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","product":"marnie-runner-2-7x10","blockedItems":["blocked_sku1","blocked_sku2"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"blockedItems":["blocked_sku1","blocked_sku2"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -487,10 +487,15 @@ describe('Recommend Api', () => { return { tag: index.toString(), limit: 20, + filters: [ + { field: 'color', type: '=', values: ['blue'] }, + { field: 'price', type: '>=', values: [0] }, + { field: 'price', type: '<=', values: [20] }, + ], }; }), siteId: '8uyt2m', - product: 'marnie-runner-2-7x10', + products: ['marnie-runner-2-7x10'], lastViewed: [ 'marnie-runner-2-7x10', 'ruby-runner-2-7x10', @@ -508,11 +513,6 @@ describe('Recommend Api', () => { 'dayna-4x6', 'moema-4x6', ], - filters: [ - { field: 'color', type: '=', values: ['blue'] }, - { field: 'price', type: '>=', values: [0] }, - { field: 'price', type: '<=', values: [20] }, - ], }), }; diff --git a/packages/snap-client/src/Client/apis/Recommend.ts b/packages/snap-client/src/Client/apis/Recommend.ts index 32cea86c1..593d3c559 100644 --- a/packages/snap-client/src/Client/apis/Recommend.ts +++ b/packages/snap-client/src/Client/apis/Recommend.ts @@ -1,8 +1,8 @@ import { API, ApiConfiguration } from './Abstract'; -import { HTTPHeaders, PostRecommendRequestFiltersModel } from '../../types'; +import { HTTPHeaders, RecommendPostRequestFiltersModel, RecommendPostRequestProfileModel } from '../../types'; import { AppMode } from '@searchspring/snap-toolbox'; import { transformRecommendationFiltersPost } from '../transforms'; -import { ProfileRequestModel, ProfileResponseModel, RecommendResponseModel, RecommendRequestModel, PostRecommendAPISpec } from '../../types'; +import { ProfileRequestModel, ProfileResponseModel, RecommendResponseModel, RecommendRequestModel, RecommendPostRequestModel } from '../../types'; class Deferred { promise: Promise; @@ -27,7 +27,7 @@ export class RecommendAPI extends API { private batches: { [key: string]: { timeout: number | NodeJS.Timeout; - request: PostRecommendAPISpec; + request: RecommendPostRequestModel; entries: BatchEntry[]; }; }; @@ -61,22 +61,8 @@ export class RecommendAPI extends API { const batch = (this.batches[key] = this.batches[key] || { timeout: null, request: { profiles: [] }, entries: [] }); const deferred = new Deferred(); - const { tag, limits, limit, query, filters, profileFilters, dedupe, categories, brands } = parameters; - - const newParams = { - ...parameters, - tag: tag, - categories, - brands, - limit: limit ? limit : limits ? (typeof limits == 'number' ? limits : limits[0]) : undefined, - searchTerm: query, - profileFilters, - dedupe, - filters, - }; - // add each request to the list - batch.entries.push({ request: newParams, deferred: deferred }); + batch.entries.push({ request: parameters, deferred: deferred }); // wait for all of the requests to come in const timeoutClear = typeof window !== 'undefined' ? window.clearTimeout : clearTimeout; @@ -92,29 +78,24 @@ export class RecommendAPI extends API { // now that the requests are in proper order, map through them // and build out the batches batch.entries.map((entry) => { - const { tag, categories, brands, query, profileFilters, dedupe } = entry.request; - let limit = entry.request.limit; + const { tag, categories, brands, query, filters, dedupe } = entry.request; - if (!limit) { - limit = 20; + let transformedFilters; + if (filters) { + transformedFilters = transformRecommendationFiltersPost(filters) as RecommendPostRequestFiltersModel[]; } - const profile: any = { + const profile: RecommendPostRequestProfileModel = { tag, categories, brands, - limit, - query, - filters: profileFilters, + limit: entry.request.limit || 20, + searchTerm: query, + filters: transformedFilters, dedupe, }; - let transformedFilters; - if (profile.filters) { - transformedFilters = transformRecommendationFiltersPost(profile.filters) as PostRecommendRequestFiltersModel[]; - } - - batch.request.profiles?.push({ ...profile, filters: transformedFilters }); + batch.request.profiles?.push(profile); batch.request = { ...batch.request, @@ -126,20 +107,18 @@ export class RecommendAPI extends API { cart: parameters.cart, lastViewed: parameters.lastViewed, shopper: parameters.shopper, - } as PostRecommendAPISpec; + } as RecommendPostRequestModel; - // combine product data if both 'product' and 'products' are used - if (batch.request.product && Array.isArray(batch.request.products) && batch.request.products.indexOf(batch.request.product) == -1) { - batch.request.products = batch.request.products.concat(batch.request.product); + // use products request only and combine when needed + if (batch.request.product) { + if (Array.isArray(batch.request.products) && batch.request.products.indexOf(batch.request.product) == -1) { + batch.request.products = batch.request.products.concat(batch.request.product); + } else { + batch.request.products = [batch.request.product]; + } delete batch.request.product; } - - // transform global filters here - if (entry.request.filters) { - const globalFiltersToAdd = transformRecommendationFiltersPost(entry.request.filters!) as PostRecommendRequestFiltersModel[]; - batch.request['filters'] = globalFiltersToAdd; - } }); try { @@ -151,7 +130,7 @@ export class RecommendAPI extends API { batch.request['product'] = batch.request['product'].toString(); } - const response = await this.postRecommendations(batch.request as PostRecommendAPISpec); + const response = await this.postRecommendations(batch.request as RecommendPostRequestModel); batch.entries?.forEach((entry, index) => { entry.deferred.resolve([response[index]]); @@ -166,7 +145,7 @@ export class RecommendAPI extends API { return deferred.promise; } - async postRecommendations(requestParameters: PostRecommendAPISpec): Promise { + async postRecommendations(requestParameters: RecommendPostRequestModel): Promise { const headerParameters: HTTPHeaders = {}; headerParameters['Content-Type'] = 'text/plain'; diff --git a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts index 6b04e447f..3b164690a 100644 --- a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts +++ b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts @@ -1,7 +1,7 @@ -import { RecommendationRequestFilterModel, PostRecommendRequestFiltersModel } from '../../types'; +import { RecommendationRequestFilterModel, RecommendPostRequestFiltersModel } from '../../types'; export const transformRecommendationFiltersPost = (filters: RecommendationRequestFilterModel[]) => { - const filterArray: PostRecommendRequestFiltersModel[] = []; + const filterArray: RecommendPostRequestFiltersModel[] = []; filters.map((filter) => { if (filter.type == 'value') { //check if filterArray contains a filter for this value already diff --git a/packages/snap-client/src/types.ts b/packages/snap-client/src/types.ts index 4c3020068..5c1d50cae 100644 --- a/packages/snap-client/src/types.ts +++ b/packages/snap-client/src/types.ts @@ -104,7 +104,7 @@ export type TrendingResponseModel = { export type RecommendRequestModel = { tag: string; - siteId: string; + siteId?: string; product?: string; products?: string[]; shopper?: string; @@ -113,23 +113,21 @@ export type RecommendRequestModel = { cart?: string[]; lastViewed?: string[]; test?: boolean; + branch?: string; + filters?: RecommendationRequestFilterModel[]; + blockedItems?: string[]; batched?: boolean; - limits?: number | number[]; limit?: number; order?: number; - filters?: RecommendationRequestFilterModel[]; - blockedItems?: string[]; batchId?: number; - profileFilters?: RecommendationRequestFilterModel[]; query?: string; dedupe?: boolean; - branch?: string; }; //TODO: move to snapi -export type PostRecommendAPISpec = { +export type RecommendPostRequestModel = { siteId: string; - profiles: (Omit & { filters?: PostRecommendRequestFiltersModel[] })[]; + profiles: RecommendPostRequestProfileModel[]; product?: string; products?: string[]; shopper?: string; @@ -138,10 +136,15 @@ export type PostRecommendAPISpec = { test?: boolean; withRecInfo?: boolean; blockedItems?: string[]; - filters?: PostRecommendRequestFiltersModel[]; + filters?: RecommendPostRequestFiltersModel[]; +}; + +export type RecommendPostRequestProfileModel = Omit & { + filters?: RecommendPostRequestFiltersModel[]; + searchTerm?: string; }; -export type RecommendPostProfileObject = { +export type RecommendRequestProfileModel = { tag: string; categories?: string[]; brands?: string[]; @@ -151,7 +154,7 @@ export type RecommendPostProfileObject = { filters?: RecommendationRequestFilterModel[]; }; -export type PostRecommendRequestFiltersModel = { +export type RecommendPostRequestFiltersModel = { field: string; type: '=' | '==' | '===' | '!=' | '!==' | '>' | '<' | '>=' | '<='; values: (string | number)[]; diff --git a/packages/snap-preact-demo/src/components/Recommendations/Recs/Recs.tsx b/packages/snap-preact-demo/src/components/Recommendations/Recs/Recs.tsx index 13b16fecd..460906692 100644 --- a/packages/snap-preact-demo/src/components/Recommendations/Recs/Recs.tsx +++ b/packages/snap-preact-demo/src/components/Recommendations/Recs/Recs.tsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import { observer } from 'mobx-react'; -import { Carousel, Recommendation, Result } from '@searchspring/snap-preact-components'; +import { Recommendation, Result } from '@searchspring/snap-preact-components'; type RecsProps = { controller?: RecommendationController; @@ -22,18 +22,12 @@ export class Recs extends Component { render() { const controller = this.props.controller; const store = controller?.store; - const arr = Array.from(Array(9).keys()); + const parameters = store?.profile?.display?.templateParameters; return (
- - {arr.map((num) => ( -
{num}!!!
- ))} -
-
- + {store.results.map((result) => ( ))} diff --git a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendationBundle.cy.js b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendationBundle.cy.js index 3820ff733..7d5e40a97 100644 --- a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendationBundle.cy.js +++ b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendationBundle.cy.js @@ -46,7 +46,7 @@ describe('BundledRecommendations', () => { describe('Tests Bundle', () => { it('has a controller', function () { cy.snapController(config?.selectors?.recommendation.controller).then(({ store }) => { - expect(store.config.globals.product.length).to.greaterThan(0); + expect(store.config.globals.products.length).to.greaterThan(0); }); }); diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx index baa7aac61..a4afa9d04 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx @@ -280,6 +280,7 @@ describe('RecommendationInstantiator', () => { document.body.innerHTML = ` @@ -455,13 +472,14 @@ describe('RecommendationInstantiator', () => { Object.keys(recommendationInstantiator.controller).forEach((controllerId, index) => { const controller = recommendationInstantiator.controller[controllerId]; expect(controller.context).toStrictEqual({ + custom: { some: 'thing' }, globals: { - product: 'C-AD-W1-1869P', - shopperId: 'snapdev', + products: ['C-AD-W1-1869P'], + shopper: { id: 'snapdev' }, blockedItems: ['1234', '5678'], cart: ['5678'], }, - ...profileContextArray[index], + profile: profileContextArray[index], }); }); const batchId = recommendationInstantiator.controller[Object.keys(recommendationInstantiator.controller)[0]].store.config.batchId; @@ -475,13 +493,18 @@ describe('RecommendationInstantiator', () => { cart: ['5678'], categories: ['1234'], limit: 1, - profileFilters: [ + filters: [ { - field: 'color', - type: 'value', - value: 'red', + field: 'price', + type: 'range', + value: { + low: 20, + high: 40, + }, }, ], + products: ['C-AD-W1-1869P'], + shopper: 'snapdev', batchId, siteId: '8uyt2m', tag: 'trending', @@ -495,15 +518,17 @@ describe('RecommendationInstantiator', () => { cart: ['5678'], categories: ['5678'], limit: 2, - profileFilters: [ + filters: [ { field: 'color', type: 'value', value: 'blue', }, ], + products: ['C-AD-W1-1869P'], + shopper: 'snapdev', batchId, - siteId: '8uyt2m', + siteId: undefined, tag: 'similar', }); }); diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx index b0eefc8fb..e6779a52b 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx @@ -51,20 +51,24 @@ type RecommendationProfileCounts = { }; type ProfileSpecificProfile = { + custom?: any; + options: Pick & { + realtime?: boolean; + }; profile: string; target: string; - options: Partial; }; type ProfileSpecificGlobals = { - products?: string[]; - siteId?: string; + blockedItems: string[]; cart?: string[] | (() => string[]); + products?: string[]; shopper?: { id?: string }; + siteId?: string; }; -type ExtendedRecommendaitonTarget = Target & { - context?: ProfileSpecificProfile; +type ExtendedRecommendaitonProfileTarget = Target & { + profile?: ProfileSpecificProfile; }; export class RecommendationInstantiator { @@ -146,92 +150,72 @@ export class RecommendationInstantiator { ], async (target: Target, elem: Element | undefined, originalElem: Element | undefined) => { const elemContext = getContext( - ['shopperId', 'shopper', 'product', 'products', 'seed', 'cart', 'options', 'profile', 'profiles', 'globals', 'custom'], + ['shopperId', 'shopper', 'product', 'products', 'seed', 'cart', 'options', 'profile', 'custom', 'profiles', 'globals'], (originalElem || elem) as HTMLScriptElement ); - const context: ContextVariables = deepmerge(this.context, elemContext); + if (elemContext.profiles && elemContext.profiles.length) { + // using the new script integration structure - const profiles = context.profiles as ProfileSpecificProfile[]; - const globals = context.globals as ProfileSpecificGlobals; + // type the new profile specific integration context variables + const scriptContextProfiles = elemContext.profiles as ProfileSpecificProfile[]; + const scriptContextGlobals = elemContext.globals as ProfileSpecificGlobals; - // controller globals and shared things + // grab from globals + const requestGlobals: Partial = { + blockedItems: scriptContextGlobals.blockedItems, + cart: scriptContextGlobals.cart && getArrayFunc(scriptContextGlobals.cart), + products: scriptContextGlobals.products, + shopper: scriptContextGlobals.shopper?.id, + siteId: scriptContextGlobals.siteId, + batchId: Math.random(), + }; - if (profiles && profiles.length) { - const targetsArr: ExtendedRecommendaitonTarget[] = []; - const batchId = Math.random(); + const targetsArr: ExtendedRecommendaitonProfileTarget[] = []; - profiles.forEach((profile) => { + // build out the targets array for each profile + scriptContextProfiles.forEach((profile) => { if (profile.target) { const targetObj = { selector: profile.target, autoRetarget: true, clickRetarget: true, - context: profile, + profile, }; targetsArr.push(targetObj); } }); - new DomTargeter(targetsArr, async (target: ExtendedRecommendaitonTarget, elem: Element | undefined, originalElem: Element | undefined) => { - const profileContext: ContextVariables = deepmerge(this.context, { ...target.context, globals }); - - const { options: profileOptions } = profileContext; - const controllerGlobals: Partial = {}; - - const tag = profileContext.profile; - - // context globals - if (profileContext.globals) { - if (profileContext.globals.siteId) { - controllerGlobals.siteId = profileContext.globals.siteId; - } - if (profileContext.globals.products) { - controllerGlobals.products = profileContext.globals.products; - } - if (profileContext.globals.blockedItems) { - controllerGlobals.blockedItems = profileContext.globals.blockedItems; - } - if (profileContext.globals.cart) { - controllerGlobals.cart = profileContext.globals.cart; + new DomTargeter( + targetsArr, + async (target: ExtendedRecommendaitonProfileTarget, elem: Element | undefined, originalElem: Element | undefined) => { + if (target.profile?.profile) { + const profileRequestGlobals: RecommendRequestModel = { ...requestGlobals, ...target.profile?.options, tag: target.profile.profile }; + const profileContext: ContextVariables = deepmerge(this.context, { globals: scriptContextGlobals, profile: target.profile }); + if (elemContext.custom) { + profileContext.custom = elemContext.custom; + } + + readyTheController(this, elem, profileContext, profileCount, originalElem, profileRequestGlobals); } } - - if (profileOptions?.filters) { - controllerGlobals.profileFilters = profileOptions.filters; - } - - if (typeof profileOptions?.dedupe == 'boolean') { - controllerGlobals.dedupe = profileOptions.dedupe; - } - - readyTheController(this, elem, profileContext, profileCount, originalElem, batchId, controllerGlobals, tag); - }); + ); } else { - const { product, seed, options } = context; - - const controllerGlobals: any = {}; - - const tag = elem?.getAttribute('searchspring-recommend'); - - if (product || seed) { - controllerGlobals.product = product || seed; - } - - if (options?.siteId) { - controllerGlobals.siteId = options.siteId; - } - - if (options?.branch) { - controllerGlobals.branch = options.branch; - } - - if (options?.filters) { - controllerGlobals.filters = options.filters; - } - - readyTheController(this, elem, context, profileCount, originalElem, 1, controllerGlobals, tag); + // using the "legacy" method + const { profile, products, product, seed, options, batched, shopper, shopperId } = elemContext; + + const profileRequestGlobals: Partial = { + tag: profile, + batched: batched ?? true, + batchId: 1, + products: products || (product && [product]) || (seed && [seed]), + cart: elemContext.cart && getArrayFunc(elemContext.cart), + shopper: shopper?.id || shopperId, + ...options, + }; + + readyTheController(this, elem, elemContext, profileCount, originalElem, profileRequestGlobals); } } ); @@ -256,76 +240,38 @@ async function readyTheController( context: ContextVariables, profileCount: RecommendationProfileCounts, elem: Element | undefined, - batchId: number, - controllerGlobals: Partial, - tag: string | null | undefined + controllerGlobals: Partial ) { - const { shopper, shopperId, products, cart, options } = context; - const blockedItems = context.options?.blockedItems; + const { batched, batchId, realtime, cart, tag } = controllerGlobals; if (!tag) { // FEEDBACK: change message depending on script integration type (profile vs. legacy) - instance.logger.warn(`'profile' attribute is missing from