{isVisible ? (
diff --git a/packages/snap-preact-demo/public/product.html b/packages/snap-preact-demo/public/product.html
index 82281aadc..bf1ee166f 100644
--- a/packages/snap-preact-demo/public/product.html
+++ b/packages/snap-preact-demo/public/product.html
@@ -180,7 +180,7 @@
Stripe Out Off-The-Shoulder Dress
profiles = [
{
profile: 'similar',
- target: '.ss__recs__similar'
+ selector: '.ss__recs__similar'
},
]
diff --git a/packages/snap-preact-demo/public/recommendations.html b/packages/snap-preact-demo/public/recommendations.html
index fcfc8ebd9..37c9985f9 100644
--- a/packages/snap-preact-demo/public/recommendations.html
+++ b/packages/snap-preact-demo/public/recommendations.html
@@ -180,7 +180,7 @@ Stripe Out Off-The-Shoulder Dress
profiles = [
{
profile: 'similar',
- target: '.ss__recs__similar'
+ selector: '.ss__recs__similar'
},
]
diff --git a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js
index 1a5ce82f9..e5f1f60e6 100644
--- a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js
+++ b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js
@@ -66,7 +66,7 @@ describe('Recommendations', () => {
describe('Tests Recommendations', () => {
it('has a controller', function () {
cy.snapController(integration?.selectors?.recommendation.controller).then(({ store }) => {
- expect(store.config.globals.limit).equals(store.results.length);
+ expect(store.results.length).equals(20); // max limit when no limit specified
expect((store.config.globals.product || store.config.globals.products).length).to.be.greaterThan(0);
});
});
diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx
index a4afa9d04..05228a2fe 100644
--- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx
+++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx
@@ -342,28 +342,107 @@ describe('RecommendationInstantiator', () => {
expect(clientSpy).toHaveBeenCalledTimes(1);
expect(clientSpy).toHaveBeenCalledWith({
- batched: true,
+ tag: 'trending',
+ products: ['sku1'],
+ shopper: 'snapdev',
branch: 'testing',
- categories: ['cats', 'dogs'],
+ batched: true,
+ siteId: baseConfig.client?.globals.siteId,
+ profile: {
+ siteId: 'abc123',
+ branch: 'testing',
+ categories: ['cats', 'dogs'],
+ filters: [
+ {
+ type: 'value',
+ field: 'color',
+ value: 'blue',
+ },
+ {
+ type: 'range',
+ field: 'price',
+ value: { low: 0, high: 20 },
+ },
+ ],
+ brands: ['nike', 'h&m'],
+ limit: 5,
+ },
+ });
+ });
+
+ it('uses the globals from the config in the request', async () => {
+ document.body.innerHTML = ``;
+
+ const client = new MockClient(baseConfig.client!.globals, {});
+ const clientSpy = jest.spyOn(client, 'recommend');
+
+ const globalConfig = {
+ ...baseConfig,
+ client: {
+ globals: {
+ siteId: '8uyt2m',
+ filters: [
+ {
+ type: 'value',
+ field: 'color',
+ value: 'red',
+ },
+ ],
+ },
+ },
+ };
+
+ const recommendationInstantiator = new RecommendationInstantiator(globalConfig, { client });
+ await wait();
+ expect(Object.keys(recommendationInstantiator.controller).length).toBe(1);
+ Object.keys(recommendationInstantiator.controller).forEach((controllerId) => {
+ const controller = recommendationInstantiator.controller[controllerId];
+ expect(controller.context).toStrictEqual({
+ profile: 'trending',
+ options: {
+ filters: [
+ {
+ type: 'value',
+ field: 'color',
+ value: 'blue',
+ },
+ ],
+ },
+ });
+ });
+
+ expect(clientSpy).toHaveBeenCalledTimes(1);
+ expect(clientSpy).toHaveBeenCalledWith({
+ tag: 'trending',
+ branch: 'production',
+ batched: true,
+ siteId: baseConfig.client?.globals.siteId,
filters: [
{
type: 'value',
field: 'color',
- value: 'blue',
- },
- {
- type: 'range',
- field: 'price',
- value: { low: 0, high: 20 },
+ value: 'red',
},
],
- batchId: 1,
- brands: ['nike', 'h&m'],
- limit: 5,
- products: ['sku1'],
- shopper: 'snapdev',
- siteId: 'abc123',
- tag: 'trending',
+ profile: {
+ filters: [
+ {
+ type: 'value',
+ field: 'color',
+ value: 'blue',
+ },
+ ],
+ },
});
});
@@ -371,10 +450,10 @@ describe('RecommendationInstantiator', () => {
const profileContextArray = [
{
profile: 'trending',
- target: '#tout1',
+ selector: '#tout1',
custom: { some: 'thing1' },
options: {
- siteId: '8uyt2m',
+ siteId: 'abc123',
limit: 1,
categories: ['1234'],
brands: ['12345'],
@@ -392,7 +471,7 @@ describe('RecommendationInstantiator', () => {
},
{
profile: 'similar',
- target: '#tout2',
+ selector: '#tout2',
custom: { some: 'thing2' },
options: {
limit: 2,
@@ -427,10 +506,10 @@ describe('RecommendationInstantiator', () => {
profiles = [
{
profile: 'trending',
- target: '#tout1',
+ selector: '#tout1',
custom: { some: 'thing1' },
options: {
- siteId: '8uyt2m',
+ siteId: 'abc123',
limit: 1,
categories: ["1234"],
brands: ["12345"],
@@ -446,7 +525,7 @@ describe('RecommendationInstantiator', () => {
},
{
profile: 'similar',
- target: '#tout2',
+ selector: '#tout2',
custom: { some: 'thing2' },
options: {
limit: 2,
@@ -486,50 +565,55 @@ describe('RecommendationInstantiator', () => {
expect(clientSpy).toHaveBeenCalledTimes(2);
expect(clientSpy).toHaveBeenNthCalledWith(1, {
- batched: true,
- blockedItems: ['1234', '5678'],
- branch: 'production',
- brands: ['12345'],
- cart: ['5678'],
- categories: ['1234'],
- limit: 1,
- filters: [
- {
- field: 'price',
- type: 'range',
- value: {
- low: 20,
- high: 40,
- },
- },
- ],
+ tag: 'trending',
products: ['C-AD-W1-1869P'],
+ cart: ['5678'],
+ blockedItems: ['1234', '5678'],
shopper: 'snapdev',
batchId,
- siteId: '8uyt2m',
- tag: 'trending',
+ siteId: baseConfig.client?.globals.siteId,
+ branch: 'production',
+ batched: true,
+ profile: {
+ brands: ['12345'],
+ categories: ['1234'],
+ limit: 1,
+ siteId: 'abc123',
+ filters: [
+ {
+ field: 'price',
+ type: 'range',
+ value: {
+ low: 20,
+ high: 40,
+ },
+ },
+ ],
+ },
});
expect(clientSpy).toHaveBeenNthCalledWith(2, {
+ tag: 'similar',
+ products: ['C-AD-W1-1869P'],
+ shopper: 'snapdev',
+ batchId,
+ siteId: baseConfig.client?.globals.siteId,
batched: true,
blockedItems: ['1234', '5678'],
branch: 'production',
- brands: ['65432'],
cart: ['5678'],
- categories: ['5678'],
- limit: 2,
- filters: [
- {
- field: 'color',
- type: 'value',
- value: 'blue',
- },
- ],
- products: ['C-AD-W1-1869P'],
- shopper: 'snapdev',
- batchId,
- siteId: undefined,
- tag: 'similar',
+ profile: {
+ limit: 2,
+ brands: ['65432'],
+ categories: ['5678'],
+ filters: [
+ {
+ field: 'color',
+ type: 'value',
+ value: 'blue',
+ },
+ ],
+ },
});
});
diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx
index e6779a52b..606576c8c 100644
--- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx
+++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx
@@ -6,15 +6,9 @@ import { Client } from '@searchspring/snap-client';
import { Logger } from '@searchspring/snap-logger';
import { Tracker } from '@searchspring/snap-tracker';
-import type { ClientConfig, ClientGlobals, RecommendRequestModel } from '@searchspring/snap-client';
+import type { ClientConfig, ClientGlobals, RecommendRequestModel, RecommendationRequestFilterModel } from '@searchspring/snap-client';
import type { UrlTranslatorConfig } from '@searchspring/snap-url-manager';
-import type {
- AbstractController,
- RecommendationController,
- Attachments,
- ContextVariables,
- RecommendationControllerConfig,
-} from '@searchspring/snap-controller';
+import type { AbstractController, RecommendationController, Attachments, ContextVariables } from '@searchspring/snap-controller';
import type { VariantConfig } from '@searchspring/snap-store-mobx';
import type { Middleware } from '@searchspring/snap-event-manager';
import type { Target } from '@searchspring/snap-toolbox';
@@ -56,15 +50,15 @@ type ProfileSpecificProfile = {
realtime?: boolean;
};
profile: string;
- target: string;
+ selector: string;
};
type ProfileSpecificGlobals = {
+ filters?: RecommendationRequestFilterModel[];
blockedItems: string[];
cart?: string[] | (() => string[]);
products?: string[];
shopper?: { id?: string };
- siteId?: string;
};
type ExtendedRecommendaitonProfileTarget = Target & {
@@ -127,7 +121,9 @@ export class RecommendationInstantiator {
this.targeter = new DomTargeter(
[
{
- selector: this.config.selector || 'script[type="searchspring/recommend"], script[type="searchspring/personalized-recommendations"]',
+ selector: `${
+ this.config.selector || 'script[type="searchspring/recommend"], script[type="searchspring/personalized-recommendations"]'
+ }, script[type="searchspring/recommend"][profile="email"]`,
autoRetarget: true,
clickRetarget: true,
inject: {
@@ -150,12 +146,26 @@ export class RecommendationInstantiator {
],
async (target: Target, elem: Element | undefined, originalElem: Element | undefined) => {
const elemContext = getContext(
- ['shopperId', 'shopper', 'product', 'products', 'seed', 'cart', 'options', 'profile', 'custom', 'profiles', 'globals'],
+ [
+ 'shopperId',
+ 'shopper',
+ 'product',
+ 'products',
+ 'seed',
+ 'cart',
+ 'filters',
+ 'blockedItems',
+ 'options',
+ 'profile',
+ 'custom',
+ 'profiles',
+ 'globals',
+ ],
(originalElem || elem) as HTMLScriptElement
);
if (elemContext.profiles && elemContext.profiles.length) {
- // using the new script integration structure
+ // using the "grouped block" integration structure
// type the new profile specific integration context variables
const scriptContextProfiles = elemContext.profiles as ProfileSpecificProfile[];
@@ -163,21 +173,23 @@ export class RecommendationInstantiator {
// 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(),
+ ...defined({
+ blockedItems: scriptContextGlobals.blockedItems,
+ filters: scriptContextGlobals.filters,
+ cart: scriptContextGlobals.cart && getArrayFunc(scriptContextGlobals.cart),
+ products: scriptContextGlobals.products,
+ shopper: scriptContextGlobals.shopper?.id,
+ batchId: Math.random(),
+ }),
};
const targetsArr: ExtendedRecommendaitonProfileTarget[] = [];
// build out the targets array for each profile
scriptContextProfiles.forEach((profile) => {
- if (profile.target) {
+ if (profile.selector) {
const targetObj = {
- selector: profile.target,
+ selector: profile.selector,
autoRetarget: true,
clickRetarget: true,
profile,
@@ -191,7 +203,11 @@ export class RecommendationInstantiator {
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 profileRequestGlobals: RecommendRequestModel = {
+ ...requestGlobals,
+ profile: 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;
@@ -202,17 +218,19 @@ export class RecommendationInstantiator {
}
);
} else {
- // using the "legacy" method
- const { profile, products, product, seed, options, batched, shopper, shopperId } = elemContext;
+ // using the "legacy" integration structure
+ const { profile, products, product, seed, filters, blockedItems, options, 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,
+ ...defined({
+ products: products || (product && [product]) || (seed && [seed]),
+ cart: elemContext.cart && getArrayFunc(elemContext.cart),
+ shopper: shopper?.id || shopperId,
+ filters,
+ blockedItems,
+ profile: options,
+ }),
};
readyTheController(this, elem, elemContext, profileCount, originalElem, profileRequestGlobals);
@@ -240,9 +258,10 @@ async function readyTheController(
context: ContextVariables,
profileCount: RecommendationProfileCounts,
elem: Element | undefined,
- controllerGlobals: Partial
+ controllerGlobals: Partial
) {
- const { batched, batchId, realtime, cart, tag } = controllerGlobals;
+ const { profile, batchId, cart, tag } = controllerGlobals;
+ const batched = (profile?.batched || controllerGlobals.batched) ?? true;
if (!tag) {
// FEEDBACK: change message depending on script integration type (profile vs. legacy)
@@ -256,12 +275,7 @@ async function readyTheController(
profileCount[tag] = profileCount[tag] + 1 || 1;
- const defaultGlobals: Partial = {
- limit: 20,
- };
-
const globals: Partial = deepmerge.all([
- defaultGlobals,
instance.config.client?.globals || {},
instance.config.config?.globals || {},
controllerGlobals,
@@ -271,12 +285,16 @@ async function readyTheController(
id: `recommend_${tag}_${profileCount[tag] - 1}`,
tag,
batched: batched ?? true,
- realtime: Boolean(realtime),
+ realtime: Boolean(context.options?.realtime ?? context.profile?.options?.realtime),
batchId: batchId,
...instance.config.config,
globals,
};
+ if (profile?.branch) {
+ controllerConfig.branch = profile?.branch;
+ }
+
// try to find an existing controller by similar configuration
let controller = Object.keys(instance.controller)
.map((id) => instance.controller[id])
@@ -388,3 +406,19 @@ function getArrayFunc(arrayOrFunc: string[] | (() => string[])): string[] {
return [];
}
+
+type DefinedProps = {
+ [key: string]: any;
+};
+
+export function defined(properties: Record): DefinedProps {
+ const definedProps: DefinedProps = {};
+
+ Object.keys(properties).map((key) => {
+ if (properties[key] !== undefined) {
+ definedProps[key] = properties[key];
+ }
+ });
+
+ return definedProps;
+}