From 81c00f81028270faa67f2ce80ca923764eaf1bda Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Tue, 4 Jun 2024 17:46:01 -0400 Subject: [PATCH 1/7] docs(badges): add initial badges documentation --- docs/PREACT_BADGES.md | 282 ++++++++++++++++++++++++++++++++++++++++++ docs/documents.js | 7 ++ 2 files changed, 289 insertions(+) create mode 100644 docs/PREACT_BADGES.md diff --git a/docs/PREACT_BADGES.md b/docs/PREACT_BADGES.md new file mode 100644 index 000000000..fec4a84f7 --- /dev/null +++ b/docs/PREACT_BADGES.md @@ -0,0 +1,282 @@ +## Badges + +Badges are self-configured in the Searchspring Management Console. + +To displays badges the Result card must include the [OverlayBadge](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fmolecules-overlaybadge--default) and [CalloutBadge](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fmolecules-calloutbadge--default) components. + + +### OverlayBadge + +The `OverlayBadge` component wraps elements (children) that should have badges overlayed - typically the product image. + +```jsx + + + +``` + +### CalloutBadge + +The `CalloutBadge` component displays badges inline and can be placed in any position in the Result card. + +```jsx + +``` + +## Custom Badge Templates + +Custom Badge Templates can be created and sync to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu. + +First we'll initialize a new custom badge + +```sh +snapfu badges init [badgename] +``` +This will create two files: + +- `src/components/Badges/[badgename]/[badgename].jsx` - The jsx file is the badge component itself that will be displayed by `OverlayBadge` and `CalloutBadge`. The badge `tag`, `value`, and template `parameters` will be passed down as props. + +```jsx +export const CustomBadge = observer((props) => { + + const { tag, value, parameters } = props; + const {} = parameters; + + return ( +
{ value }
+ ) +}); +``` + +- `src/components/Badges/[badgename]/[badgename].json` - The json file describes the badge template and its parameters. + +```json +{ + "type": "snap/badge/default", + "name": "test", + "label": "Test Badge", + "description": "test custom template", + "component": "Test", + "locations": [ + "left", + "right", + "callout" + ], + "value": { + "enabled": true + }, + "parameters": [] +} +``` + +### Badge Template Overview + +The following badge template properties are **required**: + +`type` - should not be changed. It is utilized by the Snapfu CLI when syncing + +`name` - unique badge template identifier + +`label` - label that is displayed when selecting this badge template within the Searchspring Management Console + +`description` - badge template description + +`component` - the component name this badge template should use + +`locations` - a list of template locations this badge template can be placed in. This can be used to restrict certain badges to certain locations. See `Custom Badge Locations` section below for adding locations. See `Badge Template Locations` section below for possible values. + +`value.enabled` - boolean that when true, required a badge `value` to be provided when using this template + +`value.validations.min` - ensures `value` meets a numerical minimum or `string` / `url` length + +`value.validations.max` - ensures `value` meets a numerical maximum or `string` / `url` length + +`value.validations.regex` - ensures `value` meets a regex definition. Must also provide `value.validations.regexExplain` + +`value.validations.regexExplain` - required if using `value.validations.regex`. Describes the regex definition and is displayed as an error message if the regex validation fails + +`parameters` - a list of badge template parameters. See `Badge Template Parameters` section below for possible parameters. + +### Badge Template Locations +Badge template locations is an array of strings. + +Possible values when using default locations: `left`, `left/left`, `right`, `right/right`, `callout`, `callout/callout` + +Possible values when using **custom** locations: `left`, `left/[tag]`, `right`, `right/[tag]`, `callout`, `callout/[tag]`. See `Custom Badge Locations` section below for creating custom locations. + +For example, if the locations.json file contains the following location definition: + +```json +{ + "left": [ + { + "tag": "left", + "name": "Top Left" + }, + { + "tag": "left-bottom", + "name": "Bottom Left" + } + ], +} +``` + +To restrict a badge template to a custom location, the badge template `locations` array should contain the `tag` of the locations. Ie. `left/left-bottom` + + +### Badge Template Parameters +Badge template parameters is an array of objects. Each object is a template parameter and contains the following properties: + +`name` - unique badge location identifier + +`type` - parameter value type. Available types: `array`, `string`, `color`, `url`, `integer`, `decimal`, `boolean`, `checkbox`, `toggle`. See example below for example usage of each type. + +`label` - label that is displayed when selecting this badge location within the Searchspring Management Console + +`description` - badge location description + +`defaultValue` - optional default value that will be used unless specified when configuring a new badge rule. Must be a string regardless of different `type` options. + +`validations` - only applicable if `type` is `string`, `url`. `integer`, `decimal` + +`validations.min` - only applicable if `type` is `integer` or `decimal`. Ensures `defaultValue` or the user defined `value` meets a numerical minimum or `string` / `url` length + +`validations.max` - only applicable if `type` is `integer` or `decimal`. Ensures `defaultValue` or the user defined `value` meets a numerical maximum or `string` / `url` length + +`validations.regex` - ensures `defaultValue` or the user defined `value` meets a regex definition. Must also provide `validations.regexExplain` + +`validations.regexExplain` - required if using `validations.regex`. Describes the regex definition and is displayed as an error message if the regex validation fails + +`options` - required if `type` is `array`. Defined a list of available values to select. + +```json +{ + "parameters": [ + { + "name": "font_size", + "type": "array", + "label": "Font Size", + "description": "Select the badge font size", + "defaultValue": "14px", + "options": ["14px", "16px", "18px", "20px", "22px", "24px", "26px", "28px", "30px"] + }, + { + "name": "prefix", + "type": "string", + "label": "Prefix Symbol", + "description": "Display a prefix before the badge value. Ie. currency symbol", + "validations": { + "regex": "^[$€]$", + "regexExplain": "Only $ or € currency symbols are allowed" + } + }, + { + "name": "background", + "type": "color", + "label": "Badge background color", + "description": "Select the badge background color", + "defaultValue": "#0000ff" + }, + { + "name": "link", + "type": "url", + "label": "Redirect to URL on click", + "description": "Redirect to a URL when the badge is clicked" + }, + { + "name": "zindex", + "type": "integer", + "label": "Z-index", + "description": "Set a z-index value for the badge", + "validations": { + "min": -1, + "max": 2147483647 + } + }, + { + "name": "opacity", + "type": "decimal", + "label": "Opacity", + "description": "Badge opacity value between 0 and 1.0", + "defaultValue": "1.0", + "validations": { + "min": 0, + "max": 1 + } + }, + { + "name": "bold_value", + "type": "boolean", + "label": "Make value bold", + "description": "Should the badge value be bold text?", + "defaultValue": "false" + }, + { + "name": "show_border", + "type": "checkbox", + "label": "Show border", + "description": "Display a border around the badge", + "defaultValue": "false" + }, + { + "name": "show_shadow", + "type": "toggle", + "label": "Show shadow", + "description": "Display a shadow behind the badge", + "defaultValue": "true" + } + ] +} +``` + +## Custom Badge Locations + +Custom Badge Locations can be created and sync to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu. + +We can create custom overlay and callout locations by defining a `locations.json` file in the project path: `src/components/Badges/locations.json` + +`type` - should not be changed. It is utilized by the Snapfu CLI when syncing + +`left`, `right`, `callout` - should not be changed and always included. `left` and `right` defined overlay locations, `callout` defined callout locations + +`['left' | 'right' | 'callout'].tag` - unique badge location identifier + +`['left' | 'right' | 'callout'].name` - badge location name that is displayed when selecting this location within the Searchspring Management Console + +**important** - it is strongly recommended to keep the default location tags (ie. `left[0].tag="left"`, `right[0].tag="right"`, `callout[0].tag="callout"`) to ensure any existing badge rules are backwards compatible with additional locations. + +```json +{ + "type": "snap/badge/locations", + "left": [ + { + "tag": "left", + "name": "Top Left" + }, + { + "tag": "left-bottom", + "name": "Bottom Left" + } + ], + "right": [ + { + "tag": "right", + "name": "Top Right" + }, + { + "tag": "right-bottom", + "name": "Bottom Right" + } + ], + "callout": [ + { + "tag": "callout", + "name": "Callout" + }, + { + "tag": "callout_secondary", + "name": "Secondary Callout" + } + ] +} +``` \ No newline at end of file diff --git a/docs/documents.js b/docs/documents.js index 42a22a364..1a0193150 100644 --- a/docs/documents.js +++ b/docs/documents.js @@ -70,6 +70,13 @@ var documents = [ url: './docs/PREACT_RECOMMENDATIONS.md', searchable: true, }, + { + label: 'Badges', + route: '/start-preact-badges', + type: 'markdown', + url: './docs/PREACT_BADGES.md', + searchable: true, + }, ], }, { From 559ce2189f0f8c3c600b90127b24400ff620e998 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Wed, 5 Jun 2024 11:22:18 -0400 Subject: [PATCH 2/7] docs: add final badges docs --- docs/PREACT_BADGES.md | 123 +++++++++++++------- docs/documents.js | 7 ++ packages/snap-store-mobx/src/Meta/README.md | 46 ++++++++ 3 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 packages/snap-store-mobx/src/Meta/README.md diff --git a/docs/PREACT_BADGES.md b/docs/PREACT_BADGES.md index fec4a84f7..7ba6d6743 100644 --- a/docs/PREACT_BADGES.md +++ b/docs/PREACT_BADGES.md @@ -10,7 +10,7 @@ To displays badges the Result card must include the [OverlayBadge](https://searc The `OverlayBadge` component wraps elements (children) that should have badges overlayed - typically the product image. ```jsx - + ``` @@ -27,7 +27,9 @@ The `CalloutBadge` component displays badges inline and can be placed in any pos Custom Badge Templates can be created and sync to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu. -First we'll initialize a new custom badge +### Initialize Custom Badges + +First we'll initialize a new custom badge. The code examples on this page will use a `[badgename]` of `CustomBadge` ```sh snapfu badges init [badgename] @@ -48,15 +50,15 @@ export const CustomBadge = observer((props) => { }); ``` -- `src/components/Badges/[badgename]/[badgename].json` - The json file describes the badge template and its parameters. +- `src/components/Badges/[badgename]/[badgename].json` - The json file describes the badge template and its parameters. See `Badge Template Parameters` section below for possible parameters. ```json { "type": "snap/badge/default", - "name": "test", - "label": "Test Badge", - "description": "test custom template", - "component": "Test", + "name": "custombadge", + "label": "CustomBadge Badge", + "description": "custombadge custom template", + "component": "CustomBadge", "locations": [ "left", "right", @@ -69,6 +71,41 @@ export const CustomBadge = observer((props) => { } ``` +### Syncing Custom Badges + +Next we'll sync our custom badge - registering it to the Searchspring Management Console. + +```sh +snapfu badges sync [badgename] +``` + +### Using Custom Badges + +Finally, in order to use our custom badge component, we'll need to provide a `componentMap` prop containing a mapping of our custom components to the `OverlayBadge` and `CalloutBadge` components. + +```jsx +import { CustomBadge } from './components/Badges/CustomBadge'; + + CustomBadge + }} +> + + + + CustomBadge + }} +/> +``` + +The `componentMap` prop can also be used to overwrite the default badge components without the need of initializing and syncing a dedicated custom component. + ### Badge Template Overview The following badge template properties are **required**: @@ -110,14 +147,14 @@ For example, if the locations.json file contains the following location definiti { "left": [ { - "tag": "left", - "name": "Top Left" + "tag": "left", + "name": "Top Left" }, { - "tag": "left-bottom", - "name": "Bottom Left" + "tag": "left-bottom", + "name": "Bottom Left" } - ], + ] } ``` @@ -247,36 +284,36 @@ We can create custom overlay and callout locations by defining a `locations.json ```json { - "type": "snap/badge/locations", - "left": [ - { - "tag": "left", - "name": "Top Left" - }, - { - "tag": "left-bottom", - "name": "Bottom Left" - } - ], - "right": [ - { - "tag": "right", - "name": "Top Right" - }, - { - "tag": "right-bottom", - "name": "Bottom Right" - } - ], - "callout": [ - { - "tag": "callout", - "name": "Callout" - }, - { - "tag": "callout_secondary", - "name": "Secondary Callout" - } - ] + "type": "snap/badge/locations", + "left": [ + { + "tag": "left", + "name": "Top Left" + }, + { + "tag": "left-bottom", + "name": "Bottom Left" + } + ], + "right": [ + { + "tag": "right", + "name": "Top Right" + }, + { + "tag": "right-bottom", + "name": "Bottom Right" + } + ], + "callout": [ + { + "tag": "callout", + "name": "Callout" + }, + { + "tag": "callout_secondary", + "name": "Secondary Callout" + } + ] } ``` \ No newline at end of file diff --git a/docs/documents.js b/docs/documents.js index 1a0193150..c7cb624c3 100644 --- a/docs/documents.js +++ b/docs/documents.js @@ -297,6 +297,13 @@ var documents = [ url: './packages/snap-store-mobx/src/Abstract/README.md', searchable: true, }, + { + label: 'Meta', + route: '/package-storeMobx-meta', + type: 'markdown', + url: './packages/snap-store-mobx/src/Meta/README.md', + searchable: true, + }, { label: 'Autocomplete', route: '/package-storeMobx-autocomplete', diff --git a/packages/snap-store-mobx/src/Meta/README.md b/packages/snap-store-mobx/src/Meta/README.md new file mode 100644 index 000000000..78e98307f --- /dev/null +++ b/packages/snap-store-mobx/src/Meta/README.md @@ -0,0 +1,46 @@ +# MetaStore + +Each root store constructs a MetaStore to hold meta information about a site. + + +## `data` property +The `data` property contains the raw meta API response. + + +## `badges` property +The badges property contains a reference to `MetaBadges` class. + +# MetaBadges + +The `MetaBadges` class constructs data related to overlay badge layouts used in the `OverlayBadge` component. + +## `groups` property + +The `groups` property is a mapping of overlay groups containing data required to create a CSS `grid-template-areas` and `grid-template-columns` values. It ensures that if a custom locations mapping contains uneven length of locations in each section, the named grid areas can find a common denomination of sliced areas in the grid template. + +The default locations contain a single 'overlay' group with 1 location in each section. + +```json +{ + "overlay": { + "sections": [ + "left", + "right" + ], + "grid": [ + [ + "left", + "right" + ] + ] + } +} +``` + +To create the following overlay grid: + +```css +display: grid; +grid-template-columns: repeat(2, 1fr); +grid-template-areas: "left right"; +``` \ No newline at end of file From 3fa2934459febfd479c56685d796f5d32b4d4038 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Wed, 5 Jun 2024 11:31:17 -0400 Subject: [PATCH 3/7] docs: cleanup --- docs/PREACT_BADGES.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/PREACT_BADGES.md b/docs/PREACT_BADGES.md index 7ba6d6743..22dbe0345 100644 --- a/docs/PREACT_BADGES.md +++ b/docs/PREACT_BADGES.md @@ -108,7 +108,7 @@ The `componentMap` prop can also be used to overwrite the default badge componen ### Badge Template Overview -The following badge template properties are **required**: +#### The following badge template properties are **required**: `type` - should not be changed. It is utilized by the Snapfu CLI when syncing @@ -122,6 +122,10 @@ The following badge template properties are **required**: `locations` - a list of template locations this badge template can be placed in. This can be used to restrict certain badges to certain locations. See `Custom Badge Locations` section below for adding locations. See `Badge Template Locations` section below for possible values. +`parameters` - a list of badge template parameters. Can be an empty array to not contain template parameters. See `Badge Template Parameters` section below for possible parameters. + +#### The following badge template properties are **optional**: + `value.enabled` - boolean that when true, required a badge `value` to be provided when using this template `value.validations.min` - ensures `value` meets a numerical minimum or `string` / `url` length @@ -132,7 +136,6 @@ The following badge template properties are **required**: `value.validations.regexExplain` - required if using `value.validations.regex`. Describes the regex definition and is displayed as an error message if the regex validation fails -`parameters` - a list of badge template parameters. See `Badge Template Parameters` section below for possible parameters. ### Badge Template Locations Badge template locations is an array of strings. @@ -164,6 +167,8 @@ To restrict a badge template to a custom location, the badge template `locations ### Badge Template Parameters Badge template parameters is an array of objects. Each object is a template parameter and contains the following properties: +#### The following badge template parameters properties are **required**: + `name` - unique badge location identifier `type` - parameter value type. Available types: `array`, `string`, `color`, `url`, `integer`, `decimal`, `boolean`, `checkbox`, `toggle`. See example below for example usage of each type. @@ -172,7 +177,12 @@ Badge template parameters is an array of objects. Each object is a template para `description` - badge location description -`defaultValue` - optional default value that will be used unless specified when configuring a new badge rule. Must be a string regardless of different `type` options. +`options` - required only if `type` is `array`. Defines a list of available values to select. + + +#### The following badge template parameters properties are **optional**: + +`defaultValue` - default value that will be used unless specified when configuring a new badge rule. Must be a string regardless of different `type` options. `validations` - only applicable if `type` is `string`, `url`. `integer`, `decimal` @@ -184,7 +194,6 @@ Badge template parameters is an array of objects. Each object is a template para `validations.regexExplain` - required if using `validations.regex`. Describes the regex definition and is displayed as an error message if the regex validation fails -`options` - required if `type` is `array`. Defined a list of available values to select. ```json { From c10c3685aaee50255f6d50f6044fa03da243da0d Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Wed, 5 Jun 2024 16:44:39 -0400 Subject: [PATCH 4/7] docs: pr feedback --- docs/PREACT_BADGES.md | 112 +++++++++++++------- docs/documents.js | 14 +-- index.html | 4 + packages/snap-store-mobx/src/Meta/README.md | 19 ++-- 4 files changed, 98 insertions(+), 51 deletions(-) diff --git a/docs/PREACT_BADGES.md b/docs/PREACT_BADGES.md index 22dbe0345..90ef548ef 100644 --- a/docs/PREACT_BADGES.md +++ b/docs/PREACT_BADGES.md @@ -1,13 +1,13 @@ ## Badges -Badges are self-configured in the Searchspring Management Console. +Badges are self-configured in the Searchspring Management Console -To displays badges the Result card must include the [OverlayBadge](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fmolecules-overlaybadge--default) and [CalloutBadge](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fmolecules-calloutbadge--default) components. +To displays badges the Result card must include the [OverlayBadge](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fmolecules-overlaybadge--default) and [CalloutBadge](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fmolecules-calloutbadge--default) components ### OverlayBadge -The `OverlayBadge` component wraps elements (children) that should have badges overlayed - typically the product image. +The `OverlayBadge` component wraps elements (children) that should have badges overlayed - typically the product image ```jsx @@ -17,15 +17,26 @@ The `OverlayBadge` component wraps elements (children) that should have badges o ### CalloutBadge -The `CalloutBadge` component displays badges inline and can be placed in any position in the Result card. +The `CalloutBadge` component displays badges inline and can be placed in any position in the Result card ```jsx ``` +### Badge Components +The `OverlayBadge` and `CalloutBadge` components are responsible for displaying badges + +The default badges available: + +- [BadgePill](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fatoms-badgepill--default) +- [BadgeText](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fatoms-badgetext--default) +- [BadgeRectangle](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fatoms-badgerectangle--default) +- [BadgeImage](https://searchspring.github.io/snap/#/components-preact?params=%3Fpath%3D%2Fstory%2Fatoms-badgeimage--default) + + ## Custom Badge Templates -Custom Badge Templates can be created and sync to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu. +Custom Badge Templates can be created and sync to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu ### Initialize Custom Badges @@ -36,21 +47,33 @@ snapfu badges init [badgename] ``` This will create two files: -- `src/components/Badges/[badgename]/[badgename].jsx` - The jsx file is the badge component itself that will be displayed by `OverlayBadge` and `CalloutBadge`. The badge `tag`, `value`, and template `parameters` will be passed down as props. +- `src/components/Badges/[badgename]/[badgename].jsx` - The jsx file is the badge component itself that will be displayed by `OverlayBadge` and `CalloutBadge`. The badge `tag`, `value`, and template `parameters` will be passed down as props. If badge template parameters are going to be modifying css we recommend using `@emotion/react`, otherwise this can be removed ```jsx +import { css } from '@emotion/react'; +import { observer } from 'mobx-react'; + +// css in js styling using dynamic template parameters +const CSS = { + Custom: (parameters) => { + // const { bg_color } = parameters; + return css({ + // background: bg_color + }); + } +}; + export const CustomBadge = observer((props) => { const { tag, value, parameters } = props; - const {} = parameters; return ( -
{ value }
+
{ value }
) }); ``` -- `src/components/Badges/[badgename]/[badgename].json` - The json file describes the badge template and its parameters. See `Badge Template Parameters` section below for possible parameters. +- `src/components/Badges/[badgename]/[badgename].json` - The json file describes the badge template and its parameters. See `Badge Template Parameters` section below for possible parameters ```json { @@ -73,7 +96,7 @@ export const CustomBadge = observer((props) => { ### Syncing Custom Badges -Next we'll sync our custom badge - registering it to the Searchspring Management Console. +Next we'll sync our custom badge - registering it to the Searchspring Management Console ```sh snapfu badges sync [badgename] @@ -81,7 +104,9 @@ snapfu badges sync [badgename] ### Using Custom Badges -Finally, in order to use our custom badge component, we'll need to provide a `componentMap` prop containing a mapping of our custom components to the `OverlayBadge` and `CalloutBadge` components. +Finally in order to use a custom badge component, we'll need to provide a `componentMap` prop containing a mapping of our custom components to the `OverlayBadge` and `CalloutBadge` components + +**Note:** This is not required if using the default selection of badges ```jsx import { CustomBadge } from './components/Badges/CustomBadge'; @@ -104,11 +129,11 @@ import { CustomBadge } from './components/Badges/CustomBadge'; /> ``` -The `componentMap` prop can also be used to overwrite the default badge components without the need of initializing and syncing a dedicated custom component. +The `componentMap` prop can also be used to overwrite the default badge components without the need of initializing and syncing a dedicated custom component -### Badge Template Overview +### Badge Template Overview (JSON file) -#### The following badge template properties are **required**: +#### Required: `type` - should not be changed. It is utilized by the Snapfu CLI when syncing @@ -118,19 +143,19 @@ The `componentMap` prop can also be used to overwrite the default badge componen `description` - badge template description -`component` - the component name this badge template should use +`component` - component name this badge template should use. It should line up with the mapping provided to the `componentMap` props. See `Using Custom Badges` section above -`locations` - a list of template locations this badge template can be placed in. This can be used to restrict certain badges to certain locations. See `Custom Badge Locations` section below for adding locations. See `Badge Template Locations` section below for possible values. +`locations` - a list of template locations this badge template can be placed in. This can be used to restrict certain badges to certain locations. See `Custom Badge Locations` section below for adding locations. See `Badge Template Locations` section below for possible values -`parameters` - a list of badge template parameters. Can be an empty array to not contain template parameters. See `Badge Template Parameters` section below for possible parameters. +`parameters` - a list of badge template parameters. Can be an empty array to not contain template parameters. See `Badge Template Parameters` section below for possible parameters -#### The following badge template properties are **optional**: +#### Optional: `value.enabled` - boolean that when true, required a badge `value` to be provided when using this template -`value.validations.min` - ensures `value` meets a numerical minimum or `string` / `url` length +`value.validations.min` - ensures `value` meets a numerical minimum or string length -`value.validations.max` - ensures `value` meets a numerical maximum or `string` / `url` length +`value.validations.max` - ensures `value` meets a numerical maximum or string length `value.validations.regex` - ensures `value` meets a regex definition. Must also provide `value.validations.regexExplain` @@ -138,11 +163,11 @@ The `componentMap` prop can also be used to overwrite the default badge componen ### Badge Template Locations -Badge template locations is an array of strings. +Badge template locations is an array of strings Possible values when using default locations: `left`, `left/left`, `right`, `right/right`, `callout`, `callout/callout` -Possible values when using **custom** locations: `left`, `left/[tag]`, `right`, `right/[tag]`, `callout`, `callout/[tag]`. See `Custom Badge Locations` section below for creating custom locations. +Possible values when using **custom** locations: `left`, `left/[tag]`, `right`, `right/[tag]`, `callout`, `callout/[tag]`. See `Custom Badge Locations` section below for creating custom locations For example, if the locations.json file contains the following location definition: @@ -167,28 +192,36 @@ To restrict a badge template to a custom location, the badge template `locations ### Badge Template Parameters Badge template parameters is an array of objects. Each object is a template parameter and contains the following properties: -#### The following badge template parameters properties are **required**: +#### Required: -`name` - unique badge location identifier +`name` - unique badge parameter identifier -`type` - parameter value type. Available types: `array`, `string`, `color`, `url`, `integer`, `decimal`, `boolean`, `checkbox`, `toggle`. See example below for example usage of each type. +`type` - parameter value type. Available types: `array`, `string`, `color`, `url`, `integer`, `decimal`, `boolean`, `checkbox`, `toggle`. See example below for example usage of each type -`label` - label that is displayed when selecting this badge location within the Searchspring Management Console +`label` - label that is displayed when selecting this badge parameter within the Searchspring Management Console -`description` - badge location description +`description` - badge parameter description -`options` - required only if `type` is `array`. Defines a list of available values to select. +`options` - required only if `type` is `array`. Define an array of strings containing dropdown value options -#### The following badge template parameters properties are **optional**: +#### Optional: -`defaultValue` - default value that will be used unless specified when configuring a new badge rule. Must be a string regardless of different `type` options. +`defaultValue` - default value that will be used unless specified when configuring a new badge rule. Must be a string regardless of different `type` options `validations` - only applicable if `type` is `string`, `url`. `integer`, `decimal` -`validations.min` - only applicable if `type` is `integer` or `decimal`. Ensures `defaultValue` or the user defined `value` meets a numerical minimum or `string` / `url` length +`validations.min` - only applicable if `type` is `integer`, `decimal`, `string`, `url`. Should be a number (negative values also accepted) -`validations.max` - only applicable if `type` is `integer` or `decimal`. Ensures `defaultValue` or the user defined `value` meets a numerical maximum or `string` / `url` length +- If `type` is `integer` or `decimal`, ensures `defaultValue` or the user defined `value` meets a **numerical minimum** + +- If `type` is `string` or `url`, ensures `defaultValue` or the user defined `value` meets a minimum **character length** + +`validations.max` - only applicable if `type` is `integer`, `decimal`, `string`, `url`. Should be a number (negative values also accepted) + +- If `type` is `integer` or `decimal`, ensures `defaultValue` or the user defined `value` meets a **numerical maximum** + +- If `type` is `string` or `url`, ensures `defaultValue` or the user defined `value` meets a maximum **character length** `validations.regex` - ensures `defaultValue` or the user defined `value` meets a regex definition. Must also provide `validations.regexExplain` @@ -217,11 +250,11 @@ Badge template parameters is an array of objects. Each object is a template para } }, { - "name": "background", + "name": "bg_color", "type": "color", "label": "Badge background color", "description": "Select the badge background color", - "defaultValue": "#0000ff" + "defaultValue": "rgba(0,0,255,1.0)" }, { "name": "link", @@ -277,19 +310,22 @@ Badge template parameters is an array of objects. Each object is a template para ## Custom Badge Locations -Custom Badge Locations can be created and sync to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu. +Custom Badge Locations can be created and synced to the Searchspring Management Console using the Snapfu CLI. See [Getting Started > Setup](https://searchspring.github.io/snap/#/start-setup) for installing Snapfu -We can create custom overlay and callout locations by defining a `locations.json` file in the project path: `src/components/Badges/locations.json` +Custom overlay and callout locations can be created by defining a `locations.json` file in the project. It is recommended to create it at: `src/components/Badges/locations.json` `type` - should not be changed. It is utilized by the Snapfu CLI when syncing -`left`, `right`, `callout` - should not be changed and always included. `left` and `right` defined overlay locations, `callout` defined callout locations +`left`, `right`, `callout` - should not be changed and always included + +- `left` and `right` define overlay locations used by `OverlayBadge` +- `callout` define callout locations used by `CalloutBadge` `['left' | 'right' | 'callout'].tag` - unique badge location identifier `['left' | 'right' | 'callout'].name` - badge location name that is displayed when selecting this location within the Searchspring Management Console -**important** - it is strongly recommended to keep the default location tags (ie. `left[0].tag="left"`, `right[0].tag="right"`, `callout[0].tag="callout"`) to ensure any existing badge rules are backwards compatible with additional locations. +**important** - it is strongly recommended to keep the default location tags (ie. `left[0].tag="left"`, `right[0].tag="right"`, `callout[0].tag="callout"`) to ensure any existing badges are backwards compatible with additional locations ```json { diff --git a/docs/documents.js b/docs/documents.js index c7cb624c3..22d74ff0d 100644 --- a/docs/documents.js +++ b/docs/documents.js @@ -297,13 +297,6 @@ var documents = [ url: './packages/snap-store-mobx/src/Abstract/README.md', searchable: true, }, - { - label: 'Meta', - route: '/package-storeMobx-meta', - type: 'markdown', - url: './packages/snap-store-mobx/src/Meta/README.md', - searchable: true, - }, { label: 'Autocomplete', route: '/package-storeMobx-autocomplete', @@ -332,6 +325,13 @@ var documents = [ url: './packages/snap-store-mobx/src/Search/README.md', searchable: true, }, + { + label: 'Meta', + route: '/package-storeMobx-meta', + type: 'markdown', + url: './packages/snap-store-mobx/src/Meta/README.md', + searchable: true, + }, { label: 'Storage', route: '/package-storeMobx-storage', diff --git a/index.html b/index.html index 3485a5edb..e2ee9e914 100644 --- a/index.html +++ b/index.html @@ -35,6 +35,7 @@ { a: "(https://github.com/searchspring/snap/blob/main/docs/SEARCH.md)", b: "(#/advanced-search)"}, { a: "(https://github.com/searchspring/snap/blob/main/docs/PREACT_DISPLAYING_DATA.md)", b: "(#/start-preact-events)"}, + { a: "(https://github.com/searchspring/snap/blob/main/docs/PREACT_BADGES.md)", b: "(#/start-preact-badges)"}, { a: "(https://github.com/searchspring/snap/blob/main/docs/INTEGRATION.md)", b: "(#/integration)"}, { a: "(https://github.com/searchspring/snap/blob/main/docs/INTEGRATION_BACKGROUND_FILTERS.md)", b: "(#/integration-backgroundFilters)"}, { a: "(https://github.com/searchspring/snap/blob/main/docs/INTEGRATION_CONTEXT.md)", b: "(#/integration-context)"}, @@ -70,6 +71,9 @@ { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Storage)", b: "(#/package-storeMobx-storage)"}, { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Search)", b: "(#/package-storeMobx-search)"}, + { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Autocomplete)", b: "(#/package-storeMobx-autocomplete)"}, + { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Recommendation)", b: "(#/package-storeMobx-recommendation)"}, + { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Finder)", b: "(#/package-storeMobx-finder)"}, { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Abstract)", b: "(#/package-storeMobx-abstract)"}, { a: "(https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Abstract)", b: "(#/package-controller-abstract)" }, diff --git a/packages/snap-store-mobx/src/Meta/README.md b/packages/snap-store-mobx/src/Meta/README.md index 78e98307f..25da418a9 100644 --- a/packages/snap-store-mobx/src/Meta/README.md +++ b/packages/snap-store-mobx/src/Meta/README.md @@ -1,24 +1,31 @@ # MetaStore -Each root store constructs a MetaStore to hold meta information about a site. +The `MetaStore` contains the response from the Searchspring meta API which includes information about site configuration and feature settings. A `MetaStore` can be found on each root store's `meta` property. These include: + +- [SearchStore](https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Search) +- [AutocompleteStore](https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Autocomplete) +- [RecommendationStore](https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Recommendation) +- [FinderStore](https://github.com/searchspring/snap/tree/main/packages/snap-store-mobx/src/Finder) ## `data` property -The `data` property contains the raw meta API response. +The `data` property contains the raw meta API response ## `badges` property -The badges property contains a reference to `MetaBadges` class. +The badges property contains a reference to `MetaBadges` class # MetaBadges -The `MetaBadges` class constructs data related to overlay badge layouts used in the `OverlayBadge` component. +The `MetaBadges` class constructs data related to overlay badge layouts used in the `OverlayBadge` component ## `groups` property -The `groups` property is a mapping of overlay groups containing data required to create a CSS `grid-template-areas` and `grid-template-columns` values. It ensures that if a custom locations mapping contains uneven length of locations in each section, the named grid areas can find a common denomination of sliced areas in the grid template. +The `groups` property is a mapping of overlay groups used by the `OverlayBadge` component to create CSS `grid-template-areas` and `grid-template-columns` values. It ensures that if a custom location mapping contains uneven length of locations in each section, the named grid areas can find a common denomination of sliced areas in the grid template + +If you are not utilizing the `OverlayBadge` component to display [Badges](https://github.com/searchspring/snap/blob/main/docs/PREACT_BADGES.md) and creating a custom container that also utilizes css grid for overlay locations, this property can be used as a helper as it will handle changes to adding additional badge locations -The default locations contain a single 'overlay' group with 1 location in each section. +The default locations contain a single 'overlay' group with 1 location in each section ```json { From f1166815a6fc94b7d3f4c4cb6637469987012583 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 6 Jun 2024 14:22:10 -0600 Subject: [PATCH 5/7] fix(platforms/bigcommerce/addtocart): updating function to work with attributes in new groovy script --- .../bigcommerce/groovy/ss_variants.groovy | 114 +++++++++++++----- .../bigcommerce/src/addToCart.test.ts | 99 +++++++-------- .../bigcommerce/src/addToCart.ts | 75 ++++++------ 3 files changed, 162 insertions(+), 126 deletions(-) diff --git a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy index 9342475ce..27d79ea56 100644 --- a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy +++ b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy @@ -1,4 +1,4 @@ -/* +/* Snap variants script for BigCommerce Generates a JSON string with format: @@ -13,55 +13,107 @@ ] Each object (variant) in the array represents a variation of the product and each of the `options` should reflect that configuration. - When using this in Snap any properties found in `mappings.core` and `attributes` will be "masked" when the variant is selected. + When using this in Snap any properties found in `mappings.core` and `attributes` will be "masked" via result.display when the variant is selected. See Snap documentation for more details. */ import groovy.json.JsonOutput import groovy.json.JsonSlurper -import org.apache.commons.lang.StringUtils def slurper = new JsonSlurper() -def ss_variants = [] -if (Objects.nonNull(doc?.child_sku_options) && !StringUtils.isEmpty(doc?.child_sku_options)) { +/* map variants_json -> variants structure and put into array */ +def variant_array = [] + +// variant data to put into core fields +def core_fields_mapping = [ + uid: "product_id", + price: "price", + msrp: "retail_price", + sku: "child_sku", + imageUrl: 'image_url', + thumbnailImageUrl: 'image_url', +] + +// attributes outside of the options +def attributes_fields_mapping = [ + quantity: "inventory_level", // property must be named "quantity" for proper functionality +] + +def sku_options_by_id = [:]; +/* + sku_options_by_id = { + [id: string]: option[], // all options in this array have the same sku_option.id + } +*/ + +if (doc?.child_sku_options && Objects.nonNull(doc?.child_sku_options)) { def sku_options = slurper.parseText(doc.child_sku_options as String) - if(Objects.nonNull(sku_options) && !(sku_options as List).isEmpty()){ + + // build out map of sku_options_by_id options - options are grouped together by sku_option.id + if(Objects.nonNull(sku_options) && !(sku_options as List).isEmpty()) { sku_options.each { sku_option -> - def sku = [:] - def mappings = [:] - def core = [:] - def attributes = [:] - def option_data = [:] - def options = [:] - - core.put("imageUrl" , sku_option?.image_url) - core.put("url", doc.url) - core.put("uid" ,sku_option.child_sku) - mappings.put("core", core) - sku.put("mappings",mappings) - - if(Objects.nonNull(sku_option?.inventory_level)){ - attributes.put("available", sku_option?.inventory_level > 0) - } + sku_options_by_id[sku_option.id] = sku_options_by_id[sku_option.id] ?: []; + sku_options_by_id[sku_option.id].push(sku_option); + } + } - if(Objects.nonNull(sku_option?.option) && !StringUtils.isEmpty(sku_option?.option) && Objects.nonNull(sku_option?.value) && !StringUtils.isEmpty(sku_option?.value)){ - attributes.put("title", sku_option?.option + " / " + sku_option?.value) + // use sku_options_by_id map to poppulate variant_array + sku_options_by_id.each { id, options -> + def variant_object = [:] + variant_object.mappings = [:] + variant_object.mappings.core = [:] + variant_object.attributes = [:] + variant_object.options = [:] + // convert into a variant object + /* + { + "mappings": { + "core": { ... } + }, + "attributes": { + ... + } } - sku.put("attributes",attributes) + */ - option_data.put("value", sku_option?.value) + // loop through each option_array + options.each { option -> + /* populate core mappings */ + core_fields_mapping.each { core_field_name, variant_field_name -> + if (option[variant_field_name] && Objects.nonNull(option[variant_field_name])) { + variant_object.mappings.core[core_field_name] = option[variant_field_name] + } + } - if(Objects.nonNull(sku_option?.option)){ - options.put(sku_option?.option, option_data) + /* populate attributes */ + attributes_fields_mapping.each { attribute_field_name, variant_field_name -> + if (option[variant_field_name] && Objects.nonNull(option[variant_field_name])) { + variant_object.attributes[attribute_field_name] = option[variant_field_name] + } } - sku.put("options",options) - ss_variants.add(sku) + // determine availability + if (option.inventory_level > 0 && !option.purchasing_disabled) { + variant_object.attributes.available = true + } else { + variant_object.attributes.available = false + } + + /* populate options */ + if (option.option && option.value && option.option_id && option.option_value_id) { + variant_object.options[option.option] = [ + value: option.value, + optionId: option.option_value_id, + attributeId: option.option_id, + ] + } } + + variant_array.push(variant_object); } } -index.put("ss_variants", JsonOutput.toJson(ss_variants)) \ No newline at end of file +index.ss_variants = JsonOutput.toJson(variant_array) \ No newline at end of file diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts index 86f8f9b0d..cd1fe957e 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts @@ -88,15 +88,13 @@ describe('addToCart', () => { const item = results[0] as Product; addToCart([item]); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -112,16 +110,13 @@ describe('addToCart', () => { addToCart([item]); - const obj = { - product_id: item.id, - quantity: 4, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -134,23 +129,20 @@ describe('addToCart', () => { it('can use alternate id column', () => { const config = { - idFieldName: 'mappings.core.url', + idFieldName: 'mappings.core.sku', }; const item = results[0] as Product; addToCart([item], config); - const obj = { - product_id: item.mappings.core?.url, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', `${item.mappings.core?.sku}`); + formData.append('qty[]', `${item.quantity}`); const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -164,15 +156,13 @@ describe('addToCart', () => { addToCart([item]); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -193,15 +183,13 @@ describe('addToCart', () => { addToCart([item], config); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -223,15 +211,13 @@ describe('addToCart', () => { addToCart([item], config); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -249,15 +235,14 @@ describe('addToCart', () => { addToCart(items); for (let i = 0; i < items.length; i++) { - const obj = { - product_id: items[i].id, - quantity: items[i].quantity, - action: 'add', - }; + const item = items[i]; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.ts b/packages/snap-platforms/bigcommerce/src/addToCart.ts index 47c96767c..275c7a897 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.ts @@ -8,10 +8,7 @@ type BigCommerceAddToCartConfig = { type LineItem = { product_id: string; quantity: number; -}; - -type FormData = { - line_items: LineItem[]; + attributes: { attributeId?: string; optionId?: string }[]; }; export const addToCart = async (items: Product[], config?: BigCommerceAddToCartConfig) => { @@ -20,9 +17,7 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC return; } - const formData: FormData = { - line_items: [], - }; + const lineItems: LineItem[] = []; items.map((item) => { let id = item?.display?.mappings?.core?.uid; @@ -44,19 +39,32 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC } if (id && item.quantity) { - const obj = { + const productDetails: LineItem = { product_id: id, quantity: item.quantity, + attributes: [], }; - formData.line_items.push(obj); + const options = item.variants?.active?.options; + if (options) { + Object.keys(options).forEach((option) => { + const attributeId = options[option].attributeId; + const optionId = options[option].optionId; + + if (attributeId && optionId) { + productDetails.attributes.push({ attributeId, optionId }); + } + }); + } + + lineItems.push(productDetails); } }); // first check how many products we are adding - if (formData.line_items.length) { - for (let i = 0; i < formData.line_items.length; i++) { - await addSingleProductv1(formData.line_items[i]); + if (lineItems.length) { + for (let i = 0; i < lineItems.length; i++) { + await addSingleProductv1(lineItems[i]); } } @@ -72,35 +80,26 @@ const addSingleProductv1 = async (item: LineItem) => { return; } - const endpoint = { - route: `/remote/v1/cart/add`, - method: 'POST', - accept: 'application/json', - content: 'application/json', - success: 200, - }; - try { - const payload = JSON.stringify({ - ...item, - action: 'add', + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', `${item.product_id}`); + formData.append('qty[]', `${item.quantity}`); + item.attributes.forEach((attribute) => { + formData.append(`attribute[${attribute.attributeId}]`, `${attribute.optionId}`); + }); + + const response = await fetch('/remote/v1/cart/add', { + method: 'POST', + body: formData, }); - const init: RequestInit = { - method: endpoint.method, - credentials: 'same-origin', - headers: { - // note: no authorization - Accept: endpoint.accept, - 'Content-Type': endpoint.content, - }, - body: payload, - }; - - const response = await fetch(endpoint.route, init); - - if (response.status !== endpoint.success) { - throw new Error(`Error: addToCart responded with ${response.status}, ${response}`); + const data = await response.json(); + + if (response.status !== 200 || data?.data?.error) { + throw new Error(`Error: addToCart responded with: ${response.status}, ${data?.data?.error || response}`); + } else { + return data; } } catch (err) { console.error(err); From b832e660a853cbd9a9bdb54a26568e035270963a Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 7 Jun 2024 08:48:56 -0600 Subject: [PATCH 6/7] refactor(platforms/bigcommerce): updated addToCart function to use storefront API and updated test --- .../bigcommerce/groovy/ss_variants.groovy | 4 +- .../bigcommerce/src/addToCart.test.ts | 439 +++++++++++++----- .../bigcommerce/src/addToCart.ts | 98 ++-- .../src/MockData/meta/tfdz6e/meta.json | 148 ++++++ .../src/MockData/search/tfdz6e/variants.json | 356 ++++++++++++++ .../src/Search/Stores/SearchResultStore.ts | 1 + 6 files changed, 892 insertions(+), 154 deletions(-) create mode 100644 packages/snap-shared/src/MockData/meta/tfdz6e/meta.json create mode 100644 packages/snap-shared/src/MockData/search/tfdz6e/variants.json diff --git a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy index 27d79ea56..e83e52b18 100644 --- a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy +++ b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy @@ -106,8 +106,8 @@ if (doc?.child_sku_options && Objects.nonNull(doc?.child_sku_options)) { if (option.option && option.value && option.option_id && option.option_value_id) { variant_object.options[option.option] = [ value: option.value, - optionId: option.option_value_id, - attributeId: option.option_id, + optionValue: option.option_value_id, + optionId: option.option_id, ] } } diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts index cd1fe957e..155be1f9b 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts @@ -1,6 +1,5 @@ import 'whatwg-fetch'; import { addToCart } from './addToCart'; -import { Product } from '@searchspring/snap-store-mobx'; import { MockClient } from '@searchspring/snap-shared'; import { SearchStore } from '@searchspring/snap-store-mobx'; import { UrlManager, QueryStringTranslator, reactLinker } from '@searchspring/snap-url-manager'; @@ -10,9 +9,15 @@ import { Logger } from '@searchspring/snap-logger'; import { Tracker } from '@searchspring/snap-tracker'; import { SearchController } from '@searchspring/snap-controller'; +import type { Product, SearchResultStore, SearchStoreConfig } from '@searchspring/snap-store-mobx'; + +const HEADERS = { 'Content-Type': 'application/json', Accept: 'application/json' }; +const MOCK_CART_ID = '123456789'; const ORIGIN = 'http://localhost'; -const ADD_ROUTE = '/remote/v1/cart/add'; -const CART_ROUTE = '/cart.php'; +const CART_ROUTE = '/api/storefront/carts'; +const CART_EXISTS_ROUTE = `/api/storefront/carts/${MOCK_CART_ID}/items`; +const REDIRECT_ROUTE = '/cart.php'; +const MOCK_ADDED_RESPONSE = { id: MOCK_CART_ID }; const wait = (time = 1) => { return new Promise((resolve) => { @@ -36,15 +41,17 @@ const searchConfigDefault = { }, settings: {}, }; -let results: any; -let controller: any; + +let results: SearchResultStore; +let controller: SearchController; let errMock: any; +let fetchMock: any; -// @ts-ignore -const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(() => Promise.resolve({ json: () => Promise.resolve([]), ok: true, status: 200 })); +const client = new MockClient(globals, {}); +// TODO: need to use variant data from BigCommerce const controllerServices: any = { - client: new MockClient(globals, {}), + client, store: new SearchStore(searchConfig, services), urlManager, eventManager: new EventManager(), @@ -63,6 +70,18 @@ describe('addToCart', () => { results = controller.store.results; errMock = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // @ts-ignore + fetchMock = jest.spyOn(global, 'fetch').mockImplementation((url) => { + let response: any = []; + if (url == CART_ROUTE) { + response = [{ id: MOCK_CART_ID }]; + } else if (url == CART_EXISTS_ROUTE) { + response = MOCK_ADDED_RESPONSE; + } + + return Promise.resolve({ json: () => Promise.resolve(response), ok: true, status: 200 }); + }); }); beforeEach(() => { @@ -77,180 +96,368 @@ describe('addToCart', () => { }); it('requires product(s) to be passed', () => { - // @ts-ignore + // @ts-ignore - adding with no params addToCart(); expect(fetchMock).not.toHaveBeenCalled(); expect(errMock).toHaveBeenCalledWith('Error: no products to add'); }); - it('adds data passed', () => { - const item = results[0] as Product; - addToCart([item]); - - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); - - const params = { - body: formData, - method: 'POST', + it('will log an error when it cannot find a custom id', async () => { + const config = { + idFieldName: 'mappings.dne.nope', }; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + const item = results[0] as Product; + + await addToCart([item], config); - fetchMock.mockClear(); + expect(errMock).toHaveBeenCalledWith(`Error: couldnt find column in item data. please verify 'idFieldName' in the config.`); + expect(fetchMock).not.toHaveBeenCalled(); }); - it('can add multiple quantities', () => { + it('will redirect by default', async () => { const item = results[0] as Product; - item.quantity = 4; + await addToCart([item]); + await wait(10); - addToCart([item]); + expect(window.location.href).toEqual(REDIRECT_ROUTE); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + fetchMock.mockClear(); + }); - const params = { - body: formData, - method: 'POST', + it('will not redirect if config is false', async () => { + const item = results[0] as Product; + const config = { + redirect: false, }; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + await addToCart([item], config); + await wait(10); - fetchMock.mockClear(); + expect(window.location.href).toEqual(ORIGIN); - item.quantity = 1; + fetchMock.mockClear(); }); - it('can use alternate id column', () => { + it('can use a custom redirect', async () => { const config = { - idFieldName: 'mappings.core.sku', + redirect: 'https://redirect.localhost', }; const item = results[0] as Product; - addToCart([item], config); + await addToCart([item], config); + await wait(10); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', `${item.mappings.core?.sku}`); - formData.append('qty[]', `${item.quantity}`); + expect(window.location.href).toEqual(config.redirect); - const params = { - body: formData, - method: 'POST', - }; + fetchMock.mockClear(); + }); - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + it('will return the API response after adding', async () => { + const item = results[0] as Product; + + const response = await addToCart([item]); + + expect(response).toStrictEqual(MOCK_ADDED_RESPONSE); fetchMock.mockClear(); }); - it('will redirect by default', async () => { - const item = results[0] as Product; + describe('when a cart exists', () => { + it('can add a single simple product', async () => { + const item = results[0] as Product; - addToCart([item]); + await addToCart([item]); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + const getParams = { + headers: HEADERS, + method: 'GET', + }; - const params = { - body: formData, - method: 'POST', - }; + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + const postBody = { + lineItems: [ + { + product_id: item.id, + quantity: item.quantity, + }, + ], + }; - await wait(10); + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; - expect(window.location.href).toEqual(CART_ROUTE); + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); - fetchMock.mockClear(); - }); + fetchMock.mockClear(); + }); - it('will not redirect if config is false', async () => { - const item = results[0] as Product; - const config = { - redirect: false, - }; + it('can add a single product with options', async () => { + client.mockData.updateConfig({ siteId: 'tfdz6e', search: 'variants' }); + const optionSearchConfig: SearchStoreConfig = { + ...searchConfig, + settings: { + redirects: { + singleResult: false, + }, + variants: { + field: 'ss_variants', + }, + }, + }; + const optionController = new SearchController(optionSearchConfig, controllerServices); - addToCart([item], config); + await optionController.search(); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + const results = optionController.store.results; - const params = { - body: formData, - method: 'POST', - }; + const item = results[0] as Product; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + await addToCart([item]); - await wait(10); + const getParams = { + headers: HEADERS, + method: 'GET', + }; - expect(window.location.href).toEqual(ORIGIN); + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: [ + { + product_id: item.display.mappings.core?.uid, + quantity: item.quantity, + optionSelections: [ + { + optionId: 570, + optionValue: 2900, + }, + ], + }, + ], + }; - fetchMock.mockClear(); - }); + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; - it('can use a custom redirect', async () => { - const config = { - redirect: 'https://redirect.localhost', - }; + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); - const item = results[0] as Product; + fetchMock.mockClear(); + }); - addToCart([item], config); + it('can add multiple items', async () => { + const items = results.slice(0, 3) as Product[]; + items.forEach((item) => item.quantity++); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + await addToCart(items); - const params = { - body: formData, - method: 'POST', - }; + const getParams = { + headers: HEADERS, + method: 'GET', + }; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); - await wait(10); + const postBody = { + lineItems: items.map((item) => ({ + product_id: item.id, + quantity: item.quantity, + })), + }; - expect(window.location.href).toEqual(config.redirect); + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; - fetchMock.mockClear(); + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); + + it('can use alternate id column', async () => { + const config = { + idFieldName: 'mappings.core.sku', + }; + + const item = results[0] as Product; + + await addToCart([item], config); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: [ + { + product_id: item.mappings.core?.sku, + quantity: item.quantity, + }, + ], + }; + + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; + + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); }); - it('can add multiple items', async () => { - const items = results.slice(0, 3) as Product[]; - addToCart(items); + describe('when NO cart exists', () => { + beforeAll(() => { + // @ts-ignore + fetchMock = jest.spyOn(global, 'fetch').mockImplementation((url) => { + let response: any = []; + if (url == CART_EXISTS_ROUTE) { + response = MOCK_ADDED_RESPONSE; + } + + return Promise.resolve({ json: () => Promise.resolve(response), ok: true, status: 200 }); + }); + }); + + it('can add a single simple product', async () => { + const item = results[0] as Product; + + await addToCart([item]); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + const postBody = { + lineItems: [ + { + product_id: item.id, + quantity: item.quantity, + }, + ], + }; - const params = { - body: formData, + const postParams = { + headers: HEADERS, method: 'POST', + body: JSON.stringify(postBody), }; - await wait(10); + expect(fetchMock).toHaveBeenLastCalledWith(CART_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); + + it('can add a single product with options', async () => { + client.mockData.updateConfig({ siteId: 'tfdz6e', search: 'variants' }); + const optionSearchConfig: SearchStoreConfig = { + ...searchConfig, + settings: { + redirects: { + singleResult: false, + }, + variants: { + field: 'ss_variants', + }, + }, + }; + const optionController = new SearchController(optionSearchConfig, controllerServices); - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); - } + await optionController.search(); - fetchMock.mockClear(); + const results = optionController.store.results; + + const item = results[0] as Product; + + await addToCart([item]); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: [ + { + product_id: item.display.mappings.core?.uid, + quantity: item.quantity, + optionSelections: [ + { + optionId: 570, + optionValue: 2900, + }, + ], + }, + ], + }; + + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; + + expect(fetchMock).toHaveBeenLastCalledWith(CART_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); + + it('can add multiple items', async () => { + const items = results.slice(0, 3) as Product[]; + await addToCart(items); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: items.map((item) => ({ + product_id: item.id, + quantity: item.quantity, + })), + }; + + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; + + expect(fetchMock).toHaveBeenLastCalledWith(CART_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); }); }); diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.ts b/packages/snap-platforms/bigcommerce/src/addToCart.ts index 275c7a897..59ff2d177 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.ts @@ -8,7 +8,7 @@ type BigCommerceAddToCartConfig = { type LineItem = { product_id: string; quantity: number; - attributes: { attributeId?: string; optionId?: string }[]; + optionSelections?: { optionId?: string; optionValue?: string }[]; }; export const addToCart = async (items: Product[], config?: BigCommerceAddToCartConfig) => { @@ -26,13 +26,16 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC if (config?.idFieldName) { let level: any = item; config.idFieldName.split('.').map((field) => { - if (level[field]) { + if (level && level[field]) { level = level[field]; } else { - console.error('Error: couldnt find column in item data. please check your idFieldName is correct in the config.'); + console.error(`Error: couldnt find column in item data. please verify 'idFieldName' in the config.`); + level = undefined; + id = undefined; return; } }); + if (level && level !== item) { id = level; } @@ -42,17 +45,17 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC const productDetails: LineItem = { product_id: id, quantity: item.quantity, - attributes: [], }; const options = item.variants?.active?.options; if (options) { + productDetails.optionSelections = []; Object.keys(options).forEach((option) => { - const attributeId = options[option].attributeId; const optionId = options[option].optionId; + const optionValue = options[option].optionValue; - if (attributeId && optionId) { - productDetails.attributes.push({ attributeId, optionId }); + if (optionId && optionValue) { + productDetails.optionSelections?.push({ optionId, optionValue }); } }); } @@ -61,47 +64,70 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC } }); - // first check how many products we are adding if (lineItems.length) { - for (let i = 0; i < lineItems.length; i++) { - await addSingleProductv1(lineItems[i]); + const addToCartResponse = await addLineItemsToCart(lineItems); + + // do redirect (or not) + if (config?.redirect !== false) { + setTimeout(() => (window.location.href = typeof config?.redirect == 'string' ? config?.redirect : '/cart.php')); } - } - // do redirect (or not) - if (config?.redirect !== false) { - setTimeout(() => (window.location.href = typeof config?.redirect == 'string' ? config?.redirect : '/cart.php')); + return addToCartResponse; } }; -const addSingleProductv1 = async (item: LineItem) => { - if (!item) { - console.error('Error: no product to add'); - return; - } - +async function addLineItemsToCart(lineItems: LineItem[]): Promise { try { - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', `${item.product_id}`); - formData.append('qty[]', `${item.quantity}`); - item.attributes.forEach((attribute) => { - formData.append(`attribute[${attribute.attributeId}]`, `${attribute.optionId}`); - }); + const cartId = await getExistingCartId(); + + // if existing cartId use it, otherwise create new cart with items + let addToCartUrl = '/api/storefront/carts'; + if (cartId) { + addToCartUrl = `/api/storefront/carts/${cartId}/items`; + } - const response = await fetch('/remote/v1/cart/add', { + const body = JSON.stringify({ lineItems }); + + const response = await fetch(addToCartUrl, { method: 'POST', - body: formData, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, }); - const data = await response.json(); + if (response.status !== 200) { + throw new Error(`API rejected addToCart: ${response.status}`); + } - if (response.status !== 200 || data?.data?.error) { - throw new Error(`Error: addToCart responded with: ${response.status}, ${data?.data?.error || response}`); - } else { - return data; + const responseData = await response.json(); + + if (responseData?.id) { + // cart Id should exist now. + return responseData; } } catch (err) { - console.error(err); + console.error(`Error: could not add to cart.`, err); } -}; +} + +async function getExistingCartId(): Promise { + try { + const response = await fetch('/api/storefront/carts', { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + + const responseData = await response.json(); + + if (Array.isArray(responseData) && responseData.length) { + return responseData[0].id; + } + } catch (err) { + // error... + } +} diff --git a/packages/snap-shared/src/MockData/meta/tfdz6e/meta.json b/packages/snap-shared/src/MockData/meta/tfdz6e/meta.json new file mode 100644 index 000000000..7069718fb --- /dev/null +++ b/packages/snap-shared/src/MockData/meta/tfdz6e/meta.json @@ -0,0 +1,148 @@ +{ + "facets": { + "categories_hierarchy": { + "multiple": "single", + "display": "hierarchy", + "label": "Category", + "collapsed": true, + "hierarchyDelimiter": ">" + }, + "custom_color": { + "multiple": "or", + "display": "palette", + "label": "Color", + "collapsed": true + }, + "custom_depth": { + "multiple": "or", + "display": "list", + "label": "Depth", + "collapsed": true + }, + "custom_diameter": { + "multiple": "or", + "display": "list", + "label": "Diameter", + "collapsed": true + }, + "custom_head_diameter": { + "multiple": "or", + "display": "list", + "label": "Head Diameter", + "collapsed": true + }, + "custom_height": { + "multiple": "or", + "display": "list", + "label": "Height", + "collapsed": true + }, + "custom_hole_size": { + "multiple": "or", + "display": "list", + "label": "Hole Size", + "collapsed": true + }, + "custom_material": { + "multiple": "or", + "display": "list", + "label": "Type of Wood", + "collapsed": true + }, + "custom_shape": { + "multiple": "or", + "display": "list", + "label": "Shape", + "collapsed": true + }, + "custom_tenon_diameter": { + "multiple": "or", + "display": "list", + "label": "Tenon Diameter", + "collapsed": true + }, + "custom_tenon_length": { + "multiple": "or", + "display": "list", + "label": "Tenon Length", + "collapsed": true + }, + "custom_type_of_metal": { + "multiple": "or", + "display": "list", + "label": "Type of Metal", + "collapsed": true + }, + "result_depth": { + "multiple": "or", + "display": "list", + "label": "Thickness", + "collapsed": true + }, + "result_height": { + "multiple": "or", + "display": "list", + "label": "Length", + "collapsed": true + }, + "result_width": { + "multiple": "or", + "display": "list", + "label": "Width", + "collapsed": true + } + }, + "sortOptions": [ + { + "type": "relevance", + "field": "relevance", + "direction": "desc", + "label": "Best Match" + }, + { + "type": "field", + "field": "total_sold", + "direction": "desc", + "label": "Most Popular" + }, + { + "type": "field", + "field": "ss_days_since_created", + "direction": "asc", + "label": "Newest" + }, + { + "type": "field", + "field": "calculated_price", + "direction": "asc", + "label": "Price: Low to High" + }, + { + "type": "field", + "field": "calculated_price", + "direction": "desc", + "label": "Price: High to Low" + }, + { + "type": "field", + "field": "ss_diameter_inches", + "direction": "asc", + "label": "Diameter: Low to High" + }, + { + "type": "field", + "field": "ss_diameter_inches", + "direction": "desc", + "label": "Diameter: High to Low" + }, + { + "type": "field", + "field": "rating_average", + "direction": "desc", + "label": "Highest Rated" + } + ], + "pagination": { + "defaultPageSize": 72 + } +} \ No newline at end of file diff --git a/packages/snap-shared/src/MockData/search/tfdz6e/variants.json b/packages/snap-shared/src/MockData/search/tfdz6e/variants.json new file mode 100644 index 000000000..fbdef38f3 --- /dev/null +++ b/packages/snap-shared/src/MockData/search/tfdz6e/variants.json @@ -0,0 +1,356 @@ +{ + "pagination": { + "totalResults": 1, + "page": 1, + "pageSize": 30, + "totalPages": 1 + }, + "results": [ + { + "id": "4007", + "mappings": { + "core": { + "uid": "4007", + "sku": "LS-EEBP", + "name": "Easter Egg with Boho Etched Pattern", + "url": "/easter-egg-with-boho-etched-pattern/", + "price": 0, + "msrp": 0, + "imageUrl": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/27880/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44621.1.jpg", + "thumbnailImageUrl": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/27880/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44621.1.jpg", + "ratingCount": "0", + "brand": "Woodpeckers Crafts", + "popularity": "279" + } + }, + "attributes": { + "availability": "available", + "categories": [ + "Plywood and Wood Cutouts", + "Wooden Seasonal Cutouts", + "Wood Spring & Easter Cutouts", + "Shop By Season", + "Easter and Spring", + "Laser Cutouts" + ], + "categories_hierarchy": [ + "Plywood and Wood Cutouts", + "Plywood and Wood Cutouts>Wooden Seasonal Cutouts", + "Plywood and Wood Cutouts>Wooden Seasonal Cutouts>Wood Spring & Easter Cutouts", + "Shop By Season", + "Shop By Season>Easter and Spring", + "Plywood and Wood Cutouts>Laser Cutouts" + ], + "cdn_images": "28082,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__24263,jpg|27929,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44790,jpg|28010,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__22446,jpg|27877,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__70969,jpg", + "child_sku_options": "[{\"option_value_id\":2900,\"value\":\"5 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8732,\"product_id\":4007,\"child_sku\":\"LS-EEBP-5\",\"price\":0.68,\"calculated_price\":0.68,\"retail_price\":null,\"width\":3.7175,\"height\":5,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2901,\"value\":\"6 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8733,\"product_id\":4007,\"child_sku\":\"LS-EEBP-6\",\"price\":1.05,\"calculated_price\":1.05,\"retail_price\":null,\"width\":4.461,\"height\":6,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2902,\"value\":\"7 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8734,\"product_id\":4007,\"child_sku\":\"LS-EEBP-7\",\"price\":1.28,\"calculated_price\":1.28,\"retail_price\":null,\"width\":5.2045,\"height\":7,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2903,\"value\":\"8 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8735,\"product_id\":4007,\"child_sku\":\"LS-EEBP-8\",\"price\":1.79,\"calculated_price\":1.79,\"retail_price\":null,\"width\":5.948,\"height\":8,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2904,\"value\":\"9 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8736,\"product_id\":4007,\"child_sku\":\"LS-EEBP-9\",\"price\":2.35,\"calculated_price\":2.35,\"retail_price\":null,\"width\":6.6915,\"height\":9,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2905,\"value\":\"10 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8737,\"product_id\":4007,\"child_sku\":\"LS-EEBP-10\",\"price\":3.23,\"calculated_price\":3.23,\"retail_price\":null,\"width\":7.435,\"height\":10,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2906,\"value\":\"12 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8738,\"product_id\":4007,\"child_sku\":\"LS-EEBP-12\",\"price\":4.71,\"calculated_price\":4.71,\"retail_price\":null,\"width\":8.922,\"height\":12,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2907,\"value\":\"14 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8739,\"product_id\":4007,\"child_sku\":\"LS-EEBP-14\",\"price\":5.65,\"calculated_price\":5.65,\"retail_price\":null,\"width\":10.409,\"height\":14,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2908,\"value\":\"16 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8740,\"product_id\":4007,\"child_sku\":\"LS-EEBP-16\",\"price\":9.41,\"calculated_price\":9.41,\"retail_price\":null,\"width\":11.896,\"height\":16,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2909,\"value\":\"18 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8741,\"product_id\":4007,\"child_sku\":\"LS-EEBP-18\",\"price\":9.41,\"calculated_price\":9.41,\"retail_price\":null,\"width\":13.383,\"height\":18,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999}]", + "child_skus": [ + "LS-EEBP-5", + "LS-EEBP-6", + "LS-EEBP-7", + "LS-EEBP-8", + "LS-EEBP-9", + "LS-EEBP-10", + "LS-EEBP-12", + "LS-EEBP-14", + "LS-EEBP-16", + "LS-EEBP-18" + ], + "depth": "0", + "height": "0", + "id": "8f3b460ca891ef8375d35152b79d19fd", + "images": "k/383/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__24263.jpg|r/555/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44790.jpg|j/898/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__22446.jpg|p/674/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__70969.jpg", + "intellisuggestData": "eJwqSUupMktlYEhNLC5JLVJITU9XSMrPyGfwCdZ1dXUKYDBkMGQwYDBkSC_KTAEEAAD__zAJDEQ", + "intellisuggestSignature": "9b2f31f2e39929f18576d83d9d1fb072ca4928a4761e74d09ff91d0388dcbdfb", + "inventory_level": "999990", + "inventory_tracking": "sku", + "is_featured": "false", + "is_free_shipping": "false", + "is_visible": "true", + "map_price": "0", + "option_set_id": "529", + "product_type_unigram": "egg", + "rating_count": "0", + "result_depth": [ + "1/8\"" + ], + "result_height": [ + "5\"", + "6\"", + "7\"", + "8\"", + "9\"", + "10\"", + "12\"", + "14\"", + "16\"", + "18\"" + ], + "result_width": [ + "3-11/16\"", + "4-7/16\"", + "5-3/16\"", + "5-15/16\"", + "6-11/16\"", + "7-7/16\"", + "8-15/16\"", + "10-7/16\"", + "11-7/8\"", + "13-3/8\"" + ], + "reviews_count": "0", + "reviews_rating_sum": "0", + "ss_days_since_created": "129", + "ss_filter_depth": [ + "0.125" + ], + "ss_filter_height": [ + "5", + "6", + "7", + "8", + "9", + "10", + "12", + "14", + "16", + "18" + ], + "ss_filter_width": [ + "3.7175", + "4.461", + "5.2045", + "5.948", + "6.6915", + "7.435", + "8.922", + "10.409", + "11.896", + "13.383" + ], + "ss_hover_image": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/28082/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__24263.1.jpg", + "ss_image": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/27880/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44621.1.jpg", + "ss_in_stock": "1", + "ss_price_range": [ + "0.68", + "9.41" + ], + "ss_variant_depth": [ + "0.125" + ], + "ss_variant_height": [ + "5", + "6", + "7", + "8", + "9", + "10", + "12", + "14", + "16", + "18" + ], + "ss_variant_width": [ + "3.7175", + "4.461", + "5.2045", + "5.948", + "6.6915", + "7.435", + "8.922", + "10.409", + "11.896", + "13.383" + ], + "ss_variants": "[{\"mappings\":{\"core\":{\"uid\":4007,\"price\":0.68,\"sku\":\"LS-EEBP-5\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"5 Inch\",\"optionValue\":2900,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":1.05,\"sku\":\"LS-EEBP-6\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"6 Inch\",\"optionValue\":2901,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":1.28,\"sku\":\"LS-EEBP-7\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"7 Inch\",\"optionValue\":2902,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":1.79,\"sku\":\"LS-EEBP-8\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"8 Inch\",\"optionValue\":2903,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":2.35,\"sku\":\"LS-EEBP-9\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"9 Inch\",\"optionValue\":2904,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":3.23,\"sku\":\"LS-EEBP-10\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"10 Inch\",\"optionValue\":2905,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":4.71,\"sku\":\"LS-EEBP-12\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"12 Inch\",\"optionValue\":2906,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":5.65,\"sku\":\"LS-EEBP-14\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"14 Inch\",\"optionValue\":2907,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":9.41,\"sku\":\"LS-EEBP-16\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"16 Inch\",\"optionValue\":2908,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":9.41,\"sku\":\"LS-EEBP-18\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"18 Inch\",\"optionValue\":2909,\"optionId\":570}}}]", + "ss_visibility": "1", + "total_sold": "279", + "width": "0" + }, + "children": [] + } + ], + "filters": [], + "facets": [ + { + "field": "result_height", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "5\"", + "label": "5\"", + "count": 1 + }, + { + "filtered": false, + "value": "6\"", + "label": "6\"", + "count": 1 + }, + { + "filtered": false, + "value": "7\"", + "label": "7\"", + "count": 1 + }, + { + "filtered": false, + "value": "8\"", + "label": "8\"", + "count": 1 + }, + { + "filtered": false, + "value": "9\"", + "label": "9\"", + "count": 1 + }, + { + "filtered": false, + "value": "10\"", + "label": "10\"", + "count": 1 + }, + { + "filtered": false, + "value": "12\"", + "label": "12\"", + "count": 1 + }, + { + "filtered": false, + "value": "14\"", + "label": "14\"", + "count": 1 + }, + { + "filtered": false, + "value": "16\"", + "label": "16\"", + "count": 1 + }, + { + "filtered": false, + "value": "18\"", + "label": "18\"", + "count": 1 + } + ] + }, + { + "field": "result_width", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "3-11/16\"", + "label": "3-11/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "4-7/16\"", + "label": "4-7/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "5-3/16\"", + "label": "5-3/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "5-15/16\"", + "label": "5-15/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "6-11/16\"", + "label": "6-11/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "7-7/16\"", + "label": "7-7/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "8-15/16\"", + "label": "8-15/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "10-7/16\"", + "label": "10-7/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "11-7/8\"", + "label": "11-7/8\"", + "count": 1 + }, + { + "filtered": false, + "value": "13-3/8\"", + "label": "13-3/8\"", + "count": 1 + } + ] + }, + { + "field": "result_depth", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "1/8\"", + "label": "1/8\"", + "count": 1 + } + ] + }, + { + "field": "categories_hierarchy", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "Plywood and Wood Cutouts", + "label": "Plywood and Wood Cutouts", + "count": 1 + }, + { + "filtered": false, + "value": "Shop By Season", + "label": "Shop By Season", + "count": 1 + } + ] + } + ], + "sorting": [], + "merchandising": { + "redirect": "", + "content": {}, + "campaigns": [ + { + "id": "163014", + "title": "Global Boost Rules (Cutouts and 1-3/4\" Cube and Coffins and 12x12x1/4 plywood)", + "type": "global" + } + ] + }, + "search": { + "query": "easter egg boho" + } +} \ No newline at end of file diff --git a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts index 66e6cb42d..6a393975d 100644 --- a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts +++ b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts @@ -130,6 +130,7 @@ export type VariantDataOptions = Record< backgroundImageUrl?: string; attributeId?: string; optionId?: string; + optionValue?: string; } >; From 09fd97ea43d235ed75eef439b676abf1e4f65e17 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 7 Jun 2024 10:15:51 -0600 Subject: [PATCH 7/7] fix(store-mobx/searchresultstore): normalizing variant price data to ensure they are numbers --- .../src/Search/Stores/SearchResultStore.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts index 6a393975d..e0593d229 100644 --- a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts +++ b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts @@ -323,6 +323,16 @@ export class Variants { // create variants objects this.data = variantData .filter((variant) => variant.attributes.available !== false) + .map((variant) => { + // normalize price fields ensuring they are numbers + if (variant.mappings.core?.price) { + variant.mappings.core.price = Number(variant.mappings.core?.price); + } + if (variant.mappings.core?.msrp) { + variant.mappings.core.msrp = Number(variant.mappings.core?.msrp); + } + return variant; + }) .map((variant) => { Object.keys(variant.options).forEach((variantOption) => { if (!options.includes(variantOption)) {