@jay-framework/wix-stores 0.15.4 → 0.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ name: getVariantStock
2
+ description: Get variant stock availability for a product with color + text options. Returns a map of colorChoiceId -> textChoiceId -> inStock.
3
+
4
+ inputSchema:
5
+ productId: string
6
+
7
+ outputSchema: record(record(boolean))
@@ -11,6 +11,9 @@ inputSchema:
11
11
  minPrice?: number
12
12
  maxPrice?: number
13
13
  categoryIds?: string[]
14
+ optionFilters?:
15
+ - optionName: string
16
+ choiceNames: string[]
14
17
  sortBy?: enum(relevance | price_asc | price_desc | name_asc | name_desc | newest)
15
18
  cursor?: string
16
19
  pageSize?: number
@@ -31,3 +34,12 @@ outputSchema:
31
34
  maxValue?: number
32
35
  productCount: number
33
36
  isSelected: boolean
37
+ optionFilters?:
38
+ - optionId: string
39
+ optionName: string
40
+ optionRenderType: enum(TEXT_CHOICES | SWATCH_CHOICES)
41
+ choices:
42
+ - choiceId: string
43
+ choiceName: string
44
+ colorCode: string
45
+ productCount: number
@@ -180,7 +180,69 @@ tags:
180
180
  dataType: boolean
181
181
  elementType: HTMLInputElement
182
182
  description: Show only in-stock products checkbox
183
-
183
+
184
+ # Option-based filters (e.g., Color, Size)
185
+ - tag: optionFilters
186
+ type: sub-contract
187
+ repeated: true
188
+ trackBy: optionId
189
+ phase: fast+interactive
190
+ description: Filter by product options (e.g., Color, Size)
191
+ tags:
192
+ - tag: optionId
193
+ type: data
194
+ dataType: string
195
+ description: Customization ID
196
+
197
+ - tag: optionName
198
+ type: data
199
+ dataType: string
200
+ description: Option display name (e.g., Color, Size)
201
+
202
+ - tag: optionRenderType
203
+ type: variant
204
+ dataType: enum (TEXT_CHOICES | SWATCH_CHOICES)
205
+ description: How to render the option choices
206
+
207
+ - tag: choices
208
+ type: sub-contract
209
+ repeated: true
210
+ trackBy: choiceId
211
+ description: Available choices for this option
212
+ tags:
213
+ - tag: choiceId
214
+ type: data
215
+ dataType: string
216
+ description: Choice ID
217
+
218
+ - tag: choiceName
219
+ type: data
220
+ dataType: string
221
+ description: Choice display name (e.g., Red, Large)
222
+
223
+ - tag: colorCode
224
+ type: data
225
+ dataType: string
226
+ description: HEX color code (for swatch rendering)
227
+
228
+ - tag: productCount
229
+ type: data
230
+ dataType: number
231
+ phase: fast+interactive
232
+ description: Number of products with this choice in current results
233
+
234
+ - tag: isSelected
235
+ type: [data, interactive]
236
+ dataType: boolean
237
+ elementType: HTMLInputElement
238
+ description: Checkbox to toggle this choice filter
239
+
240
+ - tag: isDisabled
241
+ type: data
242
+ dataType: boolean
243
+ phase: fast+interactive
244
+ description: Whether this choice has no matching products (count=0)
245
+
184
246
  - tag: clearFilters
185
247
  type: interactive
186
248
  elementType: HTMLButtonElement
@@ -30,10 +30,32 @@ export interface CategoryFilterOfFilterOfProductSearchViewState {
30
30
  categories: Array<CategoryOfCategoryFilterOfFilterOfProductSearchViewState>
31
31
  }
32
32
 
33
+ export enum OptionRenderType {
34
+ TEXT_CHOICES,
35
+ SWATCH_CHOICES
36
+ }
37
+
38
+ export interface ChoiceOfOptionFilterOfFilterOfProductSearchViewState {
39
+ choiceId: string,
40
+ choiceName: string,
41
+ colorCode: string,
42
+ productCount: number,
43
+ isSelected: boolean,
44
+ isDisabled: boolean
45
+ }
46
+
47
+ export interface OptionFilterOfFilterOfProductSearchViewState {
48
+ optionId: string,
49
+ optionName: string,
50
+ optionRenderType: OptionRenderType,
51
+ choices: Array<ChoiceOfOptionFilterOfFilterOfProductSearchViewState>
52
+ }
53
+
33
54
  export interface FilterOfProductSearchViewState {
34
55
  priceRange: PriceRangeOfFilterOfProductSearchViewState,
35
56
  categoryFilter: CategoryFilterOfFilterOfProductSearchViewState,
36
- inStockOnly: boolean
57
+ inStockOnly: boolean,
58
+ optionFilters: Array<OptionFilterOfFilterOfProductSearchViewState>
37
59
  }
38
60
 
39
61
  export enum CurrentSort {
@@ -141,6 +163,7 @@ export type ProductSearchFastViewState = Pick<ProductSearchViewState, 'searchExp
141
163
  categoryFilter: {
142
164
  categories: Array<Pick<ProductSearchViewState['filters']['categoryFilter']['categories'][number], 'categoryId' | 'isSelected'>>;
143
165
  };
166
+ optionFilters: Array<ProductSearchViewState['filters']['optionFilters'][number]>;
144
167
  };
145
168
  sortBy: ProductSearchViewState['sortBy'];
146
169
  suggestions: Array<ProductSearchViewState['suggestions'][number]>;
@@ -153,6 +176,7 @@ export type ProductSearchInteractiveViewState = Pick<ProductSearchViewState, 'se
153
176
  categoryFilter: {
154
177
  categories: Array<Pick<ProductSearchViewState['filters']['categoryFilter']['categories'][number], 'categoryId' | 'isSelected'>>;
155
178
  };
179
+ optionFilters: Array<ProductSearchViewState['filters']['optionFilters'][number]>;
156
180
  };
157
181
  sortBy: ProductSearchViewState['sortBy'];
158
182
  suggestions: Array<ProductSearchViewState['suggestions'][number]>;
@@ -179,6 +203,11 @@ export interface ProductSearchRefs {
179
203
  categories: {
180
204
  isSelected: HTMLElementCollectionProxy<CategoryOfCategoryFilterOfFilterOfProductSearchViewState, HTMLInputElement>
181
205
  }
206
+ },
207
+ optionFilters: {
208
+ choices: {
209
+ isSelected: HTMLElementCollectionProxy<ChoiceOfOptionFilterOfFilterOfProductSearchViewState, HTMLInputElement>
210
+ }
182
211
  }
183
212
  },
184
213
  sortBy: {
@@ -210,6 +239,11 @@ export interface ProductSearchRepeatedRefs {
210
239
  categories: {
211
240
  isSelected: HTMLElementCollectionProxy<CategoryOfCategoryFilterOfFilterOfProductSearchViewState, HTMLInputElement>
212
241
  }
242
+ },
243
+ optionFilters: {
244
+ choices: {
245
+ isSelected: HTMLElementCollectionProxy<ChoiceOfOptionFilterOfFilterOfProductSearchViewState, HTMLInputElement>
246
+ }
213
247
  }
214
248
  },
215
249
  sortBy: {
@@ -313,6 +313,11 @@ function ProductPageInteractive(props, refs, viewStateSignals, fastCarryForward,
313
313
  };
314
314
  }
315
315
  const productPage = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(ProductPageInteractive);
316
+ var OptionRenderType = /* @__PURE__ */ ((OptionRenderType2) => {
317
+ OptionRenderType2[OptionRenderType2["TEXT_CHOICES"] = 0] = "TEXT_CHOICES";
318
+ OptionRenderType2[OptionRenderType2["SWATCH_CHOICES"] = 1] = "SWATCH_CHOICES";
319
+ return OptionRenderType2;
320
+ })(OptionRenderType || {});
316
321
  var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
317
322
  CurrentSort2[CurrentSort2["relevance"] = 0] = "relevance";
318
323
  CurrentSort2[CurrentSort2["priceAsc"] = 1] = "priceAsc";
@@ -369,6 +374,15 @@ function updateUrlFilters(searchTerm, filters, sort, categories) {
369
374
  }
370
375
  if (filters.inStockOnly)
371
376
  params.set("inStock", "1");
377
+ const optSegments = [];
378
+ for (const opt of filters.optionFilters || []) {
379
+ const selected = opt.choices.filter((c) => c.isSelected).map((c) => encodeURIComponent(c.choiceName));
380
+ if (selected.length > 0) {
381
+ optSegments.push(`${encodeURIComponent(opt.optionName)}:${selected.join(",")}`);
382
+ }
383
+ }
384
+ if (optSegments.length > 0)
385
+ params.set("opt", optSegments.join(";"));
372
386
  if (sort !== CurrentSort.relevance) {
373
387
  const sortNames = {
374
388
  [CurrentSort.priceAsc]: "priceAsc",
@@ -384,6 +398,30 @@ function updateUrlFilters(searchTerm, filters, sort, categories) {
384
398
  const query = params.toString();
385
399
  window.history.replaceState(null, "", query ? `?${query}` : window.location.pathname);
386
400
  }
401
+ function buildOptionFiltersViewState(baseOptionFilters, filteredResult, optionSelections) {
402
+ const filteredChoiceCounts = /* @__PURE__ */ new Map();
403
+ for (const opt of filteredResult.optionFilters || []) {
404
+ for (const ch of opt.choices) {
405
+ filteredChoiceCounts.set(ch.choiceName.toLowerCase(), ch.productCount);
406
+ }
407
+ }
408
+ return baseOptionFilters.map((opt) => ({
409
+ optionId: opt.optionId,
410
+ optionName: opt.optionName,
411
+ optionRenderType: opt.optionRenderType === "SWATCH_CHOICES" ? OptionRenderType.SWATCH_CHOICES : OptionRenderType.TEXT_CHOICES,
412
+ choices: opt.choices.map((ch) => {
413
+ const count = filteredChoiceCounts.get(ch.choiceName.toLowerCase()) ?? 0;
414
+ return {
415
+ choiceId: ch.choiceId,
416
+ choiceName: ch.choiceName,
417
+ colorCode: ch.colorCode,
418
+ productCount: count,
419
+ isSelected: optionSelections.get(opt.optionName)?.has(ch.choiceName) ?? false,
420
+ isDisabled: count === 0
421
+ };
422
+ })
423
+ }));
424
+ }
387
425
  function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForward, storesContext) {
388
426
  const baseCategoryId = fastCarryForward.baseCategoryId;
389
427
  const variantStockCache = {};
@@ -394,19 +432,41 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
394
432
  let debounceTimeout = null;
395
433
  let searchVersion = 0;
396
434
  const DEBOUNCE_MS = 300;
435
+ const [latestSearchResult, setLatestSearchResult] = createSignal(null);
436
+ const mergedFilters = createMemo(() => {
437
+ const f = filters();
438
+ const result = latestSearchResult();
439
+ if (!result)
440
+ return f;
441
+ const selections = /* @__PURE__ */ new Map();
442
+ for (const opt of f.optionFilters || []) {
443
+ const selected = opt.choices.filter((c) => c.isSelected).map((c) => c.choiceName);
444
+ if (selected.length > 0)
445
+ selections.set(opt.optionName, new Set(selected));
446
+ }
447
+ return {
448
+ ...f,
449
+ optionFilters: buildOptionFiltersViewState(fastCarryForward.baseOptionFilters, result, selections)
450
+ };
451
+ });
397
452
  const performSearch = async (version, searchTerm, currentFilters, currentSort) => {
398
453
  setIsSearching(true);
399
454
  setHasSearched(true);
400
455
  try {
401
456
  const userSelectedCategoryIds = currentFilters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => c.categoryId);
402
457
  const categoryIds = baseCategoryId ? [baseCategoryId, ...userSelectedCategoryIds] : userSelectedCategoryIds;
458
+ const activeOptionFilters = (currentFilters.optionFilters || []).map((opt) => ({
459
+ optionName: opt.optionName,
460
+ choiceNames: opt.choices.filter((c) => c.isSelected).map((c) => c.choiceName)
461
+ })).filter((o) => o.choiceNames.length > 0);
403
462
  const result = await searchProducts({
404
463
  query: searchTerm || "",
405
464
  filters: {
406
465
  minPrice: currentFilters.priceRange.minPrice || void 0,
407
466
  maxPrice: currentFilters.priceRange.maxPrice || void 0,
408
467
  categoryIds,
409
- inStockOnly: currentFilters.inStockOnly
468
+ inStockOnly: currentFilters.inStockOnly,
469
+ optionFilters: activeOptionFilters.length > 0 ? activeOptionFilters : void 0
410
470
  },
411
471
  sortBy: mapSortToAction(currentSort),
412
472
  // No cursor = start from beginning
@@ -421,6 +481,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
421
481
  setLoadedCount(result.products.length);
422
482
  setHasMore(result.hasMore);
423
483
  setHasResults(result.products.length > 0);
484
+ setLatestSearchResult(result);
424
485
  currentCursor = result.nextCursor;
425
486
  updateUrlFilters(searchTerm, currentFilters, currentSort, fastCarryForward.categories);
426
487
  } catch (error) {
@@ -443,13 +504,18 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
443
504
  const searchTerm = submittedSearchTerm();
444
505
  const userSelectedCategoryIds = currentFilters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => c.categoryId);
445
506
  const categoryIds = baseCategoryId ? [baseCategoryId, ...userSelectedCategoryIds] : userSelectedCategoryIds;
507
+ const activeOptionFilters = (currentFilters.optionFilters || []).map((opt) => ({
508
+ optionName: opt.optionName,
509
+ choiceNames: opt.choices.filter((c) => c.isSelected).map((c) => c.choiceName)
510
+ })).filter((o) => o.choiceNames.length > 0);
446
511
  const result = await searchProducts({
447
512
  query: searchTerm || "",
448
513
  filters: {
449
514
  minPrice: currentFilters.priceRange.minPrice || void 0,
450
515
  maxPrice: currentFilters.priceRange.maxPrice || void 0,
451
516
  categoryIds,
452
- inStockOnly: currentFilters.inStockOnly
517
+ inStockOnly: currentFilters.inStockOnly,
518
+ optionFilters: activeOptionFilters.length > 0 ? activeOptionFilters : void 0
453
519
  },
454
520
  sortBy: mapSortToAction(currentSort),
455
521
  cursor: currentCursor,
@@ -564,6 +630,24 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
564
630
  const isChecked = event.target.checked;
565
631
  setFilters(patch(filters(), [{ op: REPLACE, path: ["inStockOnly"], value: isChecked }]));
566
632
  });
633
+ refs.filters.optionFilters.choices.isSelected.oninput(({ event, coordinate }) => {
634
+ const [optionId, choiceId] = coordinate;
635
+ const currentFilters = filters();
636
+ const optionIndex = currentFilters.optionFilters.findIndex((o) => o.optionId === optionId);
637
+ if (optionIndex === -1)
638
+ return;
639
+ const choiceIndex = currentFilters.optionFilters[optionIndex].choices.findIndex((c) => c.choiceId === choiceId);
640
+ if (choiceIndex === -1)
641
+ return;
642
+ const isChecked = event.target.checked;
643
+ setFilters(patch(currentFilters, [
644
+ {
645
+ op: REPLACE,
646
+ path: ["optionFilters", optionIndex, "choices", choiceIndex, "isSelected"],
647
+ value: isChecked
648
+ }
649
+ ]));
650
+ });
567
651
  refs.filters.clearFilters.onclick(() => {
568
652
  const currentFilters = filters();
569
653
  const clearedCategories = currentFilters.categoryFilter.categories.map((cat) => ({
@@ -575,6 +659,10 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
575
659
  isSelected: i === 0
576
660
  // First one is "Show all"
577
661
  }));
662
+ const clearedOptionFilters = (currentFilters.optionFilters || []).map((opt) => ({
663
+ ...opt,
664
+ choices: opt.choices.map((ch) => ({ ...ch, isSelected: false }))
665
+ }));
578
666
  setFilters({
579
667
  priceRange: {
580
668
  minPrice: 0,
@@ -584,7 +672,8 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
584
672
  ranges: clearedRanges
585
673
  },
586
674
  categoryFilter: { categories: clearedCategories },
587
- inStockOnly: false
675
+ inStockOnly: false,
676
+ optionFilters: clearedOptionFilters
588
677
  });
589
678
  });
590
679
  refs.loadMoreButton.onclick(() => {
@@ -617,48 +706,48 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
617
706
  ]));
618
707
  }
619
708
  });
709
+ const variantStockLoading = /* @__PURE__ */ new Set();
620
710
  const loadVariantStock = async (productId) => {
621
- if (variantStockCache[productId])
622
- return;
623
- const stockMap = await getVariantStock({ productId });
624
- variantStockCache[productId] = stockMap;
625
- const currentResults = searchResults();
626
- const productIndex = currentResults.findIndex((p) => p._id === productId);
627
- if (productIndex === -1)
628
- return;
629
- const product = currentResults[productIndex];
630
- if (product.quickAddType !== QuickAddType.COLOR_AND_TEXT_OPTIONS)
711
+ if (variantStockCache[productId] || variantStockLoading.has(productId))
631
712
  return;
632
- const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
633
- const textChoices = product.secondQuickOption?.choices;
634
- if (!selectedColor || !textChoices)
635
- return;
636
- const colorStock = stockMap[selectedColor.choiceId];
637
- const updatedTextChoices = textChoices.map((c) => ({
638
- ...c,
639
- inStock: colorStock?.[c.choiceId] ?? false
640
- }));
641
- setSearchResults(patch(searchResults(), [
642
- {
643
- op: REPLACE,
644
- path: [productIndex, "secondQuickOption", "choices"],
645
- value: updatedTextChoices
646
- }
647
- ]));
713
+ variantStockLoading.add(productId);
714
+ try {
715
+ const currentResults = searchResults();
716
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
717
+ if (productIndex === -1)
718
+ return;
719
+ const product = currentResults[productIndex];
720
+ if (product?.quickAddType !== QuickAddType.COLOR_AND_TEXT_OPTIONS)
721
+ return;
722
+ const stockMap = await getVariantStock({ productId });
723
+ variantStockCache[productId] = stockMap;
724
+ const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
725
+ const textChoices = product.secondQuickOption?.choices;
726
+ if (!selectedColor || !textChoices)
727
+ return;
728
+ const colorStock = stockMap[selectedColor.choiceId];
729
+ const updatedTextChoices = textChoices.map((c) => ({
730
+ ...c,
731
+ inStock: colorStock?.[c.choiceId] ?? false
732
+ }));
733
+ setSearchResults(patch(searchResults(), [
734
+ {
735
+ op: REPLACE,
736
+ path: [productIndex, "secondQuickOption", "choices"],
737
+ value: updatedTextChoices
738
+ }
739
+ ]));
740
+ } finally {
741
+ variantStockLoading.delete(productId);
742
+ }
648
743
  };
649
744
  refs.searchResults.productLink.onmouseenter(({ coordinate }) => {
650
745
  const [productId] = coordinate;
651
- const product = searchResults().find((p) => p._id === productId);
652
- if (product?.quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
653
- loadVariantStock(productId);
654
- }
746
+ loadVariantStock(productId);
655
747
  });
656
748
  refs.searchResults.quickOption.choices.choiceButton.onmouseenter(({ coordinate }) => {
657
749
  const [productId] = coordinate;
658
- const product = searchResults().find((p) => p._id === productId);
659
- if (product?.quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
660
- loadVariantStock(productId);
661
- }
750
+ loadVariantStock(productId);
662
751
  });
663
752
  refs.searchResults.quickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
664
753
  const [productId, choiceId] = coordinate;
@@ -763,6 +852,10 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
763
852
  ]));
764
853
  }
765
854
  });
855
+ refs.searchResults.secondQuickOption.choices.choiceButton.onmouseenter(({ coordinate }) => {
856
+ const [productId] = coordinate;
857
+ loadVariantStock(productId);
858
+ });
766
859
  refs.searchResults.viewOptionsButton.onclick(({ coordinate }) => {
767
860
  const [productId] = coordinate;
768
861
  const product = searchResults().find((p) => p._id === productId);
@@ -780,7 +873,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
780
873
  hasResults: hasResults(),
781
874
  hasSuggestions: hasSuggestions(),
782
875
  suggestions: suggestions(),
783
- filters: filters(),
876
+ filters: mergedFilters(),
784
877
  sortBy: sortBy(),
785
878
  hasMore: hasMore(),
786
879
  loadedCount: loadedCount(),
package/dist/index.d.ts CHANGED
@@ -7,14 +7,14 @@ import * as _jay_framework_runtime from '@jay-framework/runtime';
7
7
  import { HTMLElementCollectionProxy, HTMLElementProxy } from '@jay-framework/runtime';
8
8
  import { WixClient } from '@wix/sdk';
9
9
  import { BuildDescriptors } from '@wix/sdk-types';
10
- import { productsV3, inventoryItemsV3 } from '@wix/stores';
10
+ import { productsV3, inventoryItemsV3, customizationsV3 } from '@wix/stores';
11
11
  import { categories } from '@wix/categories';
12
- import { currentCart } from '@wix/ecom';
12
+ import { Customization } from '@wix/auto_sdk_stores_customizations-v-3';
13
13
  import { Getter } from '@jay-framework/reactive';
14
14
  import { OptionChoice } from '@wix/auto_sdk_stores_products-v-3';
15
15
  import { PluginSetupContext, PluginSetupResult, PluginReferencesContext, PluginReferencesResult } from '@jay-framework/stack-server-runtime';
16
16
 
17
- declare enum OptionRenderType$1 {
17
+ declare enum OptionRenderType$2 {
18
18
  TEXT_CHOICES,
19
19
  COLOR_SWATCH_CHOICES
20
20
  }
@@ -36,7 +36,7 @@ interface ChoiceOfProductOptionsViewState {
36
36
  interface ProductOptionsViewState {
37
37
  _id: string,
38
38
  name: string,
39
- optionRenderType: OptionRenderType$1,
39
+ optionRenderType: OptionRenderType$2,
40
40
  choices: Array<ChoiceOfProductOptionsViewState>
41
41
  }
42
42
 
@@ -164,10 +164,32 @@ interface CategoryFilterOfFilterOfProductSearchViewState {
164
164
  categories: Array<CategoryOfCategoryFilterOfFilterOfProductSearchViewState>
165
165
  }
166
166
 
167
+ declare enum OptionRenderType$1 {
168
+ TEXT_CHOICES,
169
+ SWATCH_CHOICES
170
+ }
171
+
172
+ interface ChoiceOfOptionFilterOfFilterOfProductSearchViewState {
173
+ choiceId: string,
174
+ choiceName: string,
175
+ colorCode: string,
176
+ productCount: number,
177
+ isSelected: boolean,
178
+ isDisabled: boolean
179
+ }
180
+
181
+ interface OptionFilterOfFilterOfProductSearchViewState {
182
+ optionId: string,
183
+ optionName: string,
184
+ optionRenderType: OptionRenderType$1,
185
+ choices: Array<ChoiceOfOptionFilterOfFilterOfProductSearchViewState>
186
+ }
187
+
167
188
  interface FilterOfProductSearchViewState {
168
189
  priceRange: PriceRangeOfFilterOfProductSearchViewState,
169
190
  categoryFilter: CategoryFilterOfFilterOfProductSearchViewState,
170
- inStockOnly: boolean
191
+ inStockOnly: boolean,
192
+ optionFilters: Array<OptionFilterOfFilterOfProductSearchViewState>
171
193
  }
172
194
 
173
195
  declare enum CurrentSort {
@@ -275,6 +297,7 @@ type ProductSearchFastViewState = Pick<ProductSearchViewState, 'searchExpression
275
297
  categoryFilter: {
276
298
  categories: Array<Pick<ProductSearchViewState['filters']['categoryFilter']['categories'][number], 'categoryId' | 'isSelected'>>;
277
299
  };
300
+ optionFilters: Array<ProductSearchViewState['filters']['optionFilters'][number]>;
278
301
  };
279
302
  sortBy: ProductSearchViewState['sortBy'];
280
303
  suggestions: Array<ProductSearchViewState['suggestions'][number]>;
@@ -287,6 +310,7 @@ type ProductSearchInteractiveViewState = Pick<ProductSearchViewState, 'searchExp
287
310
  categoryFilter: {
288
311
  categories: Array<Pick<ProductSearchViewState['filters']['categoryFilter']['categories'][number], 'categoryId' | 'isSelected'>>;
289
312
  };
313
+ optionFilters: Array<ProductSearchViewState['filters']['optionFilters'][number]>;
290
314
  };
291
315
  sortBy: ProductSearchViewState['sortBy'];
292
316
  suggestions: Array<ProductSearchViewState['suggestions'][number]>;
@@ -313,6 +337,11 @@ interface ProductSearchRefs {
313
337
  categories: {
314
338
  isSelected: HTMLElementCollectionProxy<CategoryOfCategoryFilterOfFilterOfProductSearchViewState, HTMLInputElement>
315
339
  }
340
+ },
341
+ optionFilters: {
342
+ choices: {
343
+ isSelected: HTMLElementCollectionProxy<ChoiceOfOptionFilterOfFilterOfProductSearchViewState, HTMLInputElement>
344
+ }
316
345
  }
317
346
  },
318
347
  sortBy: {
@@ -390,14 +419,15 @@ interface WixStoresService {
390
419
  products: BuildDescriptors<typeof productsV3, {}>;
391
420
  categories: BuildDescriptors<typeof categories, {}>;
392
421
  inventory: BuildDescriptors<typeof inventoryItemsV3, {}>;
393
- /** @deprecated Use WIX_CART_SERVICE from @jay-framework/wix-cart instead */
394
- cart: BuildDescriptors<typeof currentCart, {}>;
422
+ customizations: BuildDescriptors<typeof customizationsV3, {}>;
395
423
  /** URL templates for building canonical links */
396
424
  urls: UrlTemplates;
397
425
  /** Slug of the fallback category for pages without category context */
398
426
  defaultCategory: string | null;
399
427
  /** Get the cached category tree. Lazily built on first call. */
400
428
  getCategoryTree(): Promise<CategoryTree>;
429
+ /** Get cached product customizations (options with choices). Lazily loaded. */
430
+ getCustomizations(): Promise<Customization[]>;
401
431
  }
402
432
  /**
403
433
  * Server service marker for Wix Stores.
@@ -473,6 +503,53 @@ interface WixStoresContext {
473
503
  */
474
504
  declare const WIX_STORES_CONTEXT: _jay_framework_runtime.ContextMarker<WixStoresContext>;
475
505
 
506
+ interface SearchProductsInput {
507
+ query: string;
508
+ filters?: {
509
+ inStockOnly?: boolean;
510
+ minPrice?: number;
511
+ maxPrice?: number;
512
+ categoryIds?: Array<string>;
513
+ optionFilters?: Array<{
514
+ optionName: string;
515
+ choiceNames: Array<string>;
516
+ }>;
517
+ };
518
+ sortBy?: 'relevance' | 'price_asc' | 'price_desc' | 'name_asc' | 'name_desc' | 'newest';
519
+ cursor?: string;
520
+ pageSize?: number;
521
+ }
522
+
523
+ interface SearchProductsOutput {
524
+ products: Array<ProductCardViewState>;
525
+ totalCount: number;
526
+ nextCursor?: string;
527
+ hasMore: boolean;
528
+ priceAggregation?: {
529
+ minBound: number;
530
+ maxBound: number;
531
+ ranges: Array<{
532
+ rangeId: string;
533
+ label: string;
534
+ minValue?: number;
535
+ maxValue?: number;
536
+ productCount: number;
537
+ isSelected: boolean;
538
+ }>;
539
+ };
540
+ optionFilters?: Array<{
541
+ optionId: string;
542
+ optionName: string;
543
+ optionRenderType: 'TEXT_CHOICES' | 'SWATCH_CHOICES';
544
+ choices: Array<{
545
+ choiceId: string;
546
+ choiceName: string;
547
+ colorCode: string;
548
+ productCount: number;
549
+ }>;
550
+ }>;
551
+ }
552
+
476
553
  /**
477
554
  * URL parameters for product search routes.
478
555
  * Supports: category (prefix slug), subcategory (sub-category slug).
@@ -500,6 +577,10 @@ interface SearchSlowCarryForward {
500
577
  categories: CategoryInfos;
501
578
  /** Root category ID when scoped to a category prefix (always applied, hidden from UI) */
502
579
  baseCategoryId: string | null;
580
+ /** Pre-loaded product results from slow phase (used when no query params) */
581
+ preloadedResult: SearchProductsOutput | null;
582
+ /** Base option filters from unfiltered search (static list, counts updated per search) */
583
+ baseOptionFilters: SearchProductsOutput['optionFilters'];
503
584
  }
504
585
  /**
505
586
  * Data carried forward from fast rendering to interactive phase
@@ -510,6 +591,8 @@ interface SearchFastCarryForward {
510
591
  categories: CategoryInfos;
511
592
  /** Root category ID when scoped to a category prefix (always applied, hidden from UI) */
512
593
  baseCategoryId: string | null;
594
+ /** Base option filters from unfiltered search (static list structure) */
595
+ baseOptionFilters: SearchProductsOutput['optionFilters'];
513
596
  }
514
597
  declare const productSearch: _jay_framework_fullstack_component.JayStackComponentDefinition<ProductSearchRefs, ProductSearchSlowViewState, ProductSearchFastViewState, ProductSearchInteractiveViewState, [SearchSlowCarryForward, WixStoresService], [Signals<ProductSearchFastViewState>, SearchFastCarryForward, WixStoresContext], PageProps & ProductSearchParams, ProductSearchParams, _jay_framework_component.JayComponentCore<PageProps & ProductSearchParams, ProductSearchInteractiveViewState>>;
515
598
 
@@ -857,75 +940,6 @@ declare const categoryList: _jay_framework_fullstack_component.JayStackComponent
857
940
  withInteractive(comp: _jay_framework_component.ComponentConstructor<PageProps, CategoryListRefs, CategoryListInteractiveViewState, [], _jay_framework_component.JayComponentCore<PageProps, CategoryListInteractiveViewState>>): _jay_framework_fullstack_component.JayStackComponentDefinition<CategoryListRefs, CategoryListSlowViewState, CategoryListFastViewState, CategoryListInteractiveViewState, [Record<string, never>, WixStoresService], [], PageProps, {}, _jay_framework_component.JayComponentCore<PageProps, CategoryListInteractiveViewState>>;
858
941
  };
859
942
 
860
- /**
861
- * Sort options for product search
862
- */
863
- type ProductSortField = 'relevance' | 'price_asc' | 'price_desc' | 'name_asc' | 'name_desc' | 'newest';
864
- /**
865
- * Product search filters
866
- */
867
- interface ProductSearchFilters {
868
- /** Only show products in stock */
869
- inStockOnly?: boolean;
870
- /** Minimum price filter */
871
- minPrice?: number;
872
- /** Maximum price filter */
873
- maxPrice?: number;
874
- /** Filter by category IDs */
875
- categoryIds?: string[];
876
- }
877
- /**
878
- * Price range bucket for aggregation
879
- */
880
- interface PriceRangeBucket {
881
- rangeId: string;
882
- label: string;
883
- minValue: number | null;
884
- maxValue: number | null;
885
- productCount: number;
886
- isSelected: boolean;
887
- }
888
- /**
889
- * Price aggregation data from search
890
- */
891
- interface PriceAggregationData {
892
- /** Minimum price across all products */
893
- minBound: number;
894
- /** Maximum price across all products */
895
- maxBound: number;
896
- /** Price range buckets with product counts */
897
- ranges: PriceRangeBucket[];
898
- }
899
- /**
900
- * Input for searchProducts action
901
- */
902
- interface SearchProductsInput {
903
- /** Search query text */
904
- query: string;
905
- /** Filters to apply */
906
- filters?: ProductSearchFilters;
907
- /** Sort order */
908
- sortBy?: ProductSortField;
909
- /** Cursor for pagination (from previous response's nextCursor) */
910
- cursor?: string;
911
- /** Items per page (default: 12) */
912
- pageSize?: number;
913
- }
914
- /**
915
- * Output for searchProducts action
916
- */
917
- interface SearchProductsOutput {
918
- /** List of matching products */
919
- products: ProductCardViewState[];
920
- /** Total number of matching products */
921
- totalCount: number;
922
- /** Cursor for next page (null if no more results) */
923
- nextCursor: string | null;
924
- /** Whether there are more results */
925
- hasMore: boolean;
926
- /** Price aggregation data (bounds and ranges) */
927
- priceAggregation?: PriceAggregationData;
928
- }
929
943
  /**
930
944
  * Input for getProductBySlug action
931
945
  */
@@ -1007,4 +1021,4 @@ declare function setupWixStores(ctx: PluginSetupContext): Promise<PluginSetupRes
1007
1021
  */
1008
1022
  declare function generateWixStoresReferences(ctx: PluginReferencesContext): Promise<PluginReferencesResult>;
1009
1023
 
1010
- export { type CategoryTree, type GetProductBySlugInput, type PriceAggregationData, type PriceRangeBucket, type ProductPageParams, type ProductSearchFilters, type ProductSearchParams, type ProductSortField, type SearchProductsInput, type SearchProductsOutput, type SearchSortOption, WIX_STORES_CONTEXT, WIX_STORES_SERVICE_MARKER, type WixStoresContext, type WixStoresInitData, type WixStoresService, type WixStoresServiceOptions, buildCategoryUrl, buildProductUrl, categoryList, findCategoryImage, findRootCategoryId, findRootCategorySlug, generateWixStoresReferences, getCategories, getProductBySlug, getVariantStock, init, productPage, productSearch, provideWixStoresService, searchProducts, setupWixStores };
1024
+ export { type CategoryTree, type GetProductBySlugInput, type ProductPageParams, type ProductSearchParams, type SearchSortOption, WIX_STORES_CONTEXT, WIX_STORES_SERVICE_MARKER, type WixStoresContext, type WixStoresInitData, type WixStoresService, type WixStoresServiceOptions, buildCategoryUrl, buildProductUrl, categoryList, findCategoryImage, findRootCategoryId, findRootCategorySlug, generateWixStoresReferences, getCategories, getProductBySlug, getVariantStock, init, productPage, productSearch, provideWixStoresService, searchProducts, setupWixStores };
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
- import { getCurrentCartClient } from "@jay-framework/wix-cart";
2
1
  import { WIX_CART_CONTEXT, WIX_CART_SERVICE, cartIndicator, cartPage, provideWixCartContext, provideWixCartService } from "@jay-framework/wix-cart";
3
2
  import { createJayService, makeJayQuery, ActionError, makeJayStackComponent, RenderPipeline, makeJayInit } from "@jay-framework/fullstack-component";
4
- import { inventoryItemsV3, productsV3 } from "@wix/stores";
3
+ import { customizationsV3, inventoryItemsV3, productsV3 } from "@wix/stores";
5
4
  import { categories } from "@wix/categories";
6
5
  import { registerService, getService } from "@jay-framework/stack-server-runtime";
7
6
  import { createJayContext } from "@jay-framework/runtime";
@@ -10,6 +9,11 @@ import { WIX_CLIENT_SERVICE } from "@jay-framework/wix-server-client";
10
9
  import * as fs from "fs";
11
10
  import * as path from "path";
12
11
  import * as yaml from "js-yaml";
12
+ var OptionRenderType$2 = /* @__PURE__ */ ((OptionRenderType2) => {
13
+ OptionRenderType2[OptionRenderType2["TEXT_CHOICES"] = 0] = "TEXT_CHOICES";
14
+ OptionRenderType2[OptionRenderType2["SWATCH_CHOICES"] = 1] = "SWATCH_CHOICES";
15
+ return OptionRenderType2;
16
+ })(OptionRenderType$2 || {});
13
17
  var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
14
18
  CurrentSort2[CurrentSort2["relevance"] = 0] = "relevance";
15
19
  CurrentSort2[CurrentSort2["priceAsc"] = 1] = "priceAsc";
@@ -22,7 +26,8 @@ var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
22
26
  const instances = {
23
27
  productsV3ClientInstance: void 0,
24
28
  categoriesClientInstance: void 0,
25
- inventoryV3ClientInstance: void 0
29
+ inventoryV3ClientInstance: void 0,
30
+ customizationsV3ClientInstance: void 0
26
31
  };
27
32
  function getProductsV3Client(wixClient) {
28
33
  if (!instances.productsV3ClientInstance) {
@@ -42,15 +47,23 @@ function getInventoryClient(wixClient) {
42
47
  }
43
48
  return instances.inventoryV3ClientInstance;
44
49
  }
50
+ function getCustomizationsV3Client(wixClient) {
51
+ if (!instances.customizationsV3ClientInstance) {
52
+ instances.customizationsV3ClientInstance = wixClient.use(customizationsV3);
53
+ }
54
+ return instances.customizationsV3ClientInstance;
55
+ }
45
56
  const WIX_STORES_SERVICE_MARKER = createJayService("Wix Store Service");
46
57
  function provideWixStoresService(wixClient, options) {
47
58
  let cachedTree = null;
59
+ let cachedCustomizations = null;
48
60
  const categoriesClient = getCategoriesClient(wixClient);
61
+ const customizationsClient = getCustomizationsV3Client(wixClient);
49
62
  const service = {
50
63
  products: getProductsV3Client(wixClient),
51
64
  categories: categoriesClient,
52
65
  inventory: getInventoryClient(wixClient),
53
- cart: getCurrentCartClient(wixClient),
66
+ customizations: customizationsClient,
54
67
  urls: options?.urls ?? { product: "/products/{slug}", category: null },
55
68
  defaultCategory: options?.defaultCategory ?? null,
56
69
  async getCategoryTree() {
@@ -86,6 +99,17 @@ function provideWixStoresService(wixClient, options) {
86
99
  }
87
100
  cachedTree = { slugMap, parentMap, rootIds, imageMap };
88
101
  return cachedTree;
102
+ },
103
+ async getCustomizations() {
104
+ if (cachedCustomizations) return cachedCustomizations;
105
+ try {
106
+ const result = await customizationsClient.queryCustomizations().eq("customizationType", "PRODUCT_OPTION").limit(100).find();
107
+ cachedCustomizations = result.items || [];
108
+ } catch (error) {
109
+ console.error("[wix-stores] Failed to load customizations:", error);
110
+ cachedCustomizations = [];
111
+ }
112
+ return cachedCustomizations;
89
113
  }
90
114
  };
91
115
  registerService(WIX_STORES_SERVICE_MARKER, service);
@@ -329,8 +353,8 @@ function buildVariantStockMap(product) {
329
353
  const options = product.options;
330
354
  const variants = product.variantsInfo?.variants;
331
355
  if (!options || options.length !== 2 || !variants) return stockMap;
332
- const colorOption = options.find((o) => o.optionRenderType === "COLOR_SWATCH_CHOICES");
333
- const textOption = options.find((o) => o.optionRenderType !== "COLOR_SWATCH_CHOICES");
356
+ const colorOption = options.find((o) => o.optionRenderType === "SWATCH_CHOICES");
357
+ const textOption = options.find((o) => o.optionRenderType === "TEXT_CHOICES");
334
358
  if (!colorOption || !textOption) return stockMap;
335
359
  const colorOptionId = colorOption._id || "";
336
360
  const textOptionId = textOption._id || "";
@@ -428,6 +452,31 @@ const PRICE_BUCKETS = PRICE_BUCKET_BOUNDARIES.slice(0, -1).map((from, i) => ({
428
452
  to: PRICE_BUCKET_BOUNDARIES[i + 1]
429
453
  }));
430
454
  PRICE_BUCKETS.push({ from: PRICE_BUCKET_BOUNDARIES[PRICE_BUCKET_BOUNDARIES.length - 1] });
455
+ function getAvailableProductOptions(aggResults, customizations) {
456
+ const optionNamesAgg = aggResults.find((a) => a.name === "optionNames")?.values;
457
+ const choiceNamesAgg = aggResults.find((a) => a.name === "choiceNames")?.values;
458
+ if (!optionNamesAgg?.results?.length || !choiceNamesAgg?.results?.length) {
459
+ return [];
460
+ }
461
+ const optionNames = new Set(optionNamesAgg.results.map((e) => e.value));
462
+ const choiceCounts = new Map(
463
+ choiceNamesAgg.results.map((e) => [e.value.toLowerCase(), e.count ?? 0])
464
+ );
465
+ return customizations.filter(
466
+ (c) => c.customizationType === "PRODUCT_OPTION" && c.name && optionNames.has(c.name) && (c.customizationRenderType === "TEXT_CHOICES" || c.customizationRenderType === "SWATCH_CHOICES")
467
+ ).map((c) => ({
468
+ optionId: c._id || "",
469
+ optionName: c.name || "",
470
+ optionRenderType: c.customizationRenderType,
471
+ // Sort by product count descending (most used choices first)
472
+ choices: (c.choicesSettings?.choices || []).filter((ch) => ch.name && choiceCounts.has(ch.name.toLowerCase())).map((ch) => ({
473
+ choiceId: ch._id || "",
474
+ choiceName: ch.name || "",
475
+ colorCode: ch.colorCode || "",
476
+ productCount: choiceCounts.get(ch.name.toLowerCase()) ?? 0
477
+ })).sort((a, b) => b.productCount - a.productCount)
478
+ })).filter((o) => o.choices.length > 0);
479
+ }
431
480
  const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX_STORES_SERVICE_MARKER).withHandler(
432
481
  async (input, wixStores) => {
433
482
  const { query, filters = {}, sortBy = "relevance", cursor, pageSize = 12 } = input;
@@ -460,6 +509,22 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
460
509
  }
461
510
  });
462
511
  }
512
+ if (filters.optionFilters && filters.optionFilters.length > 0) {
513
+ for (const optFilter of filters.optionFilters) {
514
+ if (optFilter.choiceNames.length > 0) {
515
+ filterConditions.push({
516
+ $and: [
517
+ { "options.name": { $hasSome: [optFilter.optionName] } },
518
+ {
519
+ "options.choicesSettings.choices.name": {
520
+ $hasSome: optFilter.choiceNames
521
+ }
522
+ }
523
+ ]
524
+ });
525
+ }
526
+ }
527
+ }
463
528
  const filter = filterConditions.length === 1 ? filterConditions[0] : { $and: filterConditions };
464
529
  const sort = [];
465
530
  switch (sortBy) {
@@ -513,6 +578,28 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
513
578
  name: "max-price",
514
579
  type: "SCALAR",
515
580
  scalar: { type: "MAX" }
581
+ },
582
+ // Option names for option-based filtering
583
+ {
584
+ fieldPath: "options.name",
585
+ name: "optionNames",
586
+ type: "VALUE",
587
+ value: {
588
+ limit: 20,
589
+ sortType: "VALUE",
590
+ sortDirection: "DESC"
591
+ }
592
+ },
593
+ // Choice names for option-based filtering
594
+ {
595
+ fieldPath: "options.choicesSettings.choices.name",
596
+ name: "choiceNames",
597
+ type: "VALUE",
598
+ value: {
599
+ limit: 50,
600
+ sortType: "COUNT",
601
+ sortDirection: "DESC"
602
+ }
516
603
  }
517
604
  ];
518
605
  const searchResult = await wixStores.products.searchProducts(
@@ -567,6 +654,8 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
567
654
  };
568
655
  });
569
656
  priceRanges.push(...bucketRanges);
657
+ const customizations = await wixStores.getCustomizations();
658
+ const optionFilters = getAvailableProductOptions(aggResults, customizations);
570
659
  const tree = await wixStores.getCategoryTree();
571
660
  const mappedProducts = products.map(
572
661
  (p) => mapProductToCard(p, wixStores.urls, tree)
@@ -580,7 +669,8 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
580
669
  minBound,
581
670
  maxBound,
582
671
  ranges: priceRanges
583
- }
672
+ },
673
+ optionFilters
584
674
  };
585
675
  } catch (error) {
586
676
  console.error("[wixStores.searchProducts] Search failed:", error);
@@ -666,13 +756,28 @@ function mapSortToAction(sort) {
666
756
  function parseUrlFilters(url) {
667
757
  try {
668
758
  const params = new URL(url, "http://x").searchParams;
759
+ const optionSelections = /* @__PURE__ */ new Map();
760
+ const optParam = params.get("opt");
761
+ if (optParam) {
762
+ for (const segment of optParam.split(";")) {
763
+ const colonIdx = segment.indexOf(":");
764
+ if (colonIdx === -1)
765
+ continue;
766
+ const name = decodeURIComponent(segment.slice(0, colonIdx));
767
+ const choices = segment.slice(colonIdx + 1).split(",").map((c) => decodeURIComponent(c)).filter(Boolean);
768
+ if (choices.length > 0) {
769
+ optionSelections.set(name, new Set(choices));
770
+ }
771
+ }
772
+ }
669
773
  return {
670
774
  searchTerm: params.get("q") || "",
671
775
  selectedCategorySlugs: params.get("cat")?.split(",").filter(Boolean) || [],
672
776
  minPrice: params.has("min") ? Number(params.get("min")) : null,
673
777
  maxPrice: params.has("max") ? Number(params.get("max")) : null,
674
778
  inStockOnly: params.get("inStock") === "1",
675
- sort: params.get("sort") || "relevance"
779
+ sort: params.get("sort") || "relevance",
780
+ optionSelections
676
781
  };
677
782
  } catch {
678
783
  return {
@@ -681,7 +786,8 @@ function parseUrlFilters(url) {
681
786
  minPrice: null,
682
787
  maxPrice: null,
683
788
  inStockOnly: false,
684
- sort: "relevance"
789
+ sort: "relevance",
790
+ optionSelections: /* @__PURE__ */ new Map()
685
791
  };
686
792
  }
687
793
  }
@@ -696,6 +802,30 @@ function parseSortParam(sort) {
696
802
  };
697
803
  return sortMap[sort] ?? CurrentSort.relevance;
698
804
  }
805
+ function buildOptionFiltersViewState(baseOptionFilters, filteredResult, optionSelections) {
806
+ const filteredChoiceCounts = /* @__PURE__ */ new Map();
807
+ for (const opt of filteredResult.optionFilters || []) {
808
+ for (const ch of opt.choices) {
809
+ filteredChoiceCounts.set(ch.choiceName.toLowerCase(), ch.productCount);
810
+ }
811
+ }
812
+ return baseOptionFilters.map((opt) => ({
813
+ optionId: opt.optionId,
814
+ optionName: opt.optionName,
815
+ optionRenderType: opt.optionRenderType === "SWATCH_CHOICES" ? OptionRenderType$2.SWATCH_CHOICES : OptionRenderType$2.TEXT_CHOICES,
816
+ choices: opt.choices.map((ch) => {
817
+ const count = filteredChoiceCounts.get(ch.choiceName.toLowerCase()) ?? 0;
818
+ return {
819
+ choiceId: ch.choiceId,
820
+ choiceName: ch.choiceName,
821
+ colorCode: ch.colorCode,
822
+ productCount: count,
823
+ isSelected: optionSelections.get(opt.optionName)?.has(ch.choiceName) ?? false,
824
+ isDisabled: count === 0
825
+ };
826
+ })
827
+ }));
828
+ }
699
829
  const EMPTY_CATEGORY_HEADER = {
700
830
  name: "",
701
831
  description: "",
@@ -801,18 +931,35 @@ async function renderSlowlyChanging$2(props, wixStores) {
801
931
  if (baseCategoryId) {
802
932
  query = query.eq("parentCategory.id", baseCategoryId);
803
933
  }
804
- const categoriesResult = await query.find();
805
- return categoriesResult.items || [];
934
+ const baseCategoryIds = baseCategoryId ? [baseCategoryId] : [];
935
+ const [categoriesResult, productsResult] = await Promise.all([
936
+ query.find(),
937
+ searchProducts({
938
+ query: "",
939
+ filters: {
940
+ categoryIds: baseCategoryIds.length > 0 ? baseCategoryIds : void 0
941
+ },
942
+ pageSize: PAGE_SIZE
943
+ })
944
+ ]);
945
+ return {
946
+ categories: categoriesResult.items || [],
947
+ productsResult
948
+ };
806
949
  }).recover((error) => {
807
- console.error("Failed to load categories:", error);
808
- return Pipeline.ok([]);
809
- }).toPhaseOutput((categories2) => {
950
+ console.error("Failed to load categories/products:", error);
951
+ return Pipeline.ok({
952
+ categories: [],
953
+ productsResult: null
954
+ });
955
+ }).toPhaseOutput(({ categories: categories2, productsResult }) => {
810
956
  const categoryInfos = categories2.map((cat) => ({
811
957
  categoryId: cat._id || "",
812
958
  categoryName: cat.name || "",
813
959
  categorySlug: cat.slug || "",
814
960
  categoryUrl: buildCategoryUrl(wixStores.urls, tree, cat.slug || "", cat._id || "") ?? ""
815
961
  }));
962
+ const baseOptionFilters = productsResult?.optionFilters || [];
816
963
  return {
817
964
  viewState: {
818
965
  searchFields: "name,description,sku",
@@ -829,7 +976,9 @@ async function renderSlowlyChanging$2(props, wixStores) {
829
976
  searchFields: "name,description,sku",
830
977
  fuzzySearch: true,
831
978
  categories: categoryInfos,
832
- baseCategoryId
979
+ baseCategoryId,
980
+ preloadedResult: productsResult,
981
+ baseOptionFilters
833
982
  }
834
983
  };
835
984
  });
@@ -839,7 +988,15 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
839
988
  const urlFilters = parseUrlFilters(props.url);
840
989
  const initialSort = parseSortParam(urlFilters.sort);
841
990
  const initialCategoryIds = urlFilters.selectedCategorySlugs.map((slug) => slowCarryForward.categories.find((c) => c.categorySlug === slug)?.categoryId).filter(Boolean);
991
+ const initialOptionFilters = [];
992
+ for (const [optionName, choiceNames] of urlFilters.optionSelections) {
993
+ initialOptionFilters.push({ optionName, choiceNames: [...choiceNames] });
994
+ }
995
+ const hasActiveFilters = !!urlFilters.searchTerm || initialCategoryIds.length > 0 || urlFilters.minPrice !== null || urlFilters.maxPrice !== null || urlFilters.inStockOnly || initialSort !== CurrentSort.relevance || initialOptionFilters.length > 0;
842
996
  return Pipeline.try(async () => {
997
+ if (!hasActiveFilters && slowCarryForward.preloadedResult) {
998
+ return slowCarryForward.preloadedResult;
999
+ }
843
1000
  const baseCategoryIds = slowCarryForward.baseCategoryId ? [slowCarryForward.baseCategoryId, ...initialCategoryIds] : initialCategoryIds;
844
1001
  const result = await searchProducts({
845
1002
  query: urlFilters.searchTerm || "",
@@ -847,7 +1004,8 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
847
1004
  categoryIds: baseCategoryIds.length > 0 ? baseCategoryIds : void 0,
848
1005
  minPrice: urlFilters.minPrice ?? void 0,
849
1006
  maxPrice: urlFilters.maxPrice ?? void 0,
850
- inStockOnly: urlFilters.inStockOnly || void 0
1007
+ inStockOnly: urlFilters.inStockOnly || void 0,
1008
+ optionFilters: initialOptionFilters.length > 0 ? initialOptionFilters : void 0
851
1009
  },
852
1010
  sortBy: initialSort !== CurrentSort.relevance ? mapSortToAction(initialSort) : void 0,
853
1011
  pageSize: PAGE_SIZE
@@ -867,13 +1025,14 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
867
1025
  {
868
1026
  rangeId: "all",
869
1027
  label: "Show all",
870
- minValue: null,
871
- maxValue: null,
1028
+ minValue: 0,
1029
+ maxValue: 1e3,
872
1030
  productCount: 0,
873
1031
  isSelected: true
874
1032
  }
875
1033
  ]
876
- }
1034
+ },
1035
+ optionFilters: []
877
1036
  });
878
1037
  }).toPhaseOutput((result) => {
879
1038
  const priceAgg = result.priceAggregation || {
@@ -883,8 +1042,8 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
883
1042
  {
884
1043
  rangeId: "all",
885
1044
  label: "Show all",
886
- minValue: null,
887
- maxValue: null,
1045
+ minValue: 0,
1046
+ maxValue: 1e3,
888
1047
  productCount: result.totalCount,
889
1048
  isSelected: true
890
1049
  }
@@ -907,14 +1066,19 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
907
1066
  maxPrice: urlFilters.maxPrice ?? priceAgg.maxBound,
908
1067
  minBound: priceAgg.minBound,
909
1068
  maxBound: priceAgg.maxBound,
910
- ranges: priceAgg.ranges
1069
+ ranges: priceAgg.ranges.map((r) => ({
1070
+ ...r,
1071
+ minValue: r.minValue ?? 0,
1072
+ maxValue: r.maxValue ?? 0
1073
+ }))
911
1074
  },
912
1075
  categoryFilter: {
913
1076
  categories: slowCarryForward.categories.map((cat) => ({
914
1077
  categoryId: cat.categoryId,
915
1078
  isSelected: initialCategoryIds.includes(cat.categoryId)
916
1079
  }))
917
- }
1080
+ },
1081
+ optionFilters: buildOptionFiltersViewState(slowCarryForward.baseOptionFilters, result, urlFilters.optionSelections)
918
1082
  },
919
1083
  sortBy: {
920
1084
  currentSort: initialSort
@@ -927,7 +1091,8 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
927
1091
  searchFields: slowCarryForward.searchFields,
928
1092
  fuzzySearch: slowCarryForward.fuzzySearch,
929
1093
  categories: slowCarryForward.categories,
930
- baseCategoryId: slowCarryForward.baseCategoryId
1094
+ baseCategoryId: slowCarryForward.baseCategoryId,
1095
+ baseOptionFilters: slowCarryForward.baseOptionFilters
931
1096
  }
932
1097
  };
933
1098
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/wix-stores",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "type": "module",
5
5
  "description": "Wix Stores API client for Jay Framework",
6
6
  "license": "Apache-2.0",
@@ -18,7 +18,8 @@
18
18
  "./category-list.jay-contract": "./dist/contracts/category-list.jay-contract",
19
19
  "./search-products.jay-action": "./dist/actions/search-products.jay-action",
20
20
  "./get-product-by-slug.jay-action": "./dist/actions/get-product-by-slug.jay-action",
21
- "./get-categories.jay-action": "./dist/actions/get-categories.jay-action"
21
+ "./get-categories.jay-action": "./dist/actions/get-categories.jay-action",
22
+ "./get-variant-stock.jay-action": "./dist/actions/get-variant-stock.jay-action"
22
23
  },
23
24
  "scripts": {
24
25
  "build": "npm run clean && npm run definitions && npm run build:client && npm run build:server && npm run build:copy-contract && npm run build:types",
@@ -33,28 +34,29 @@
33
34
  "test": ":"
34
35
  },
35
36
  "dependencies": {
36
- "@jay-framework/component": "^0.15.4",
37
- "@jay-framework/fullstack-component": "^0.15.4",
38
- "@jay-framework/reactive": "^0.15.4",
39
- "@jay-framework/runtime": "^0.15.4",
40
- "@jay-framework/secure": "^0.15.4",
41
- "@jay-framework/stack-client-runtime": "^0.15.4",
42
- "@jay-framework/stack-server-runtime": "^0.15.4",
43
- "@jay-framework/wix-cart": "^0.15.4",
44
- "@jay-framework/wix-server-client": "^0.15.4",
45
- "@jay-framework/wix-utils": "^0.15.4",
37
+ "@jay-framework/component": "^0.15.5",
38
+ "@jay-framework/fullstack-component": "^0.15.5",
39
+ "@jay-framework/reactive": "^0.15.5",
40
+ "@jay-framework/runtime": "^0.15.5",
41
+ "@jay-framework/secure": "^0.15.5",
42
+ "@jay-framework/stack-client-runtime": "^0.15.5",
43
+ "@jay-framework/stack-server-runtime": "^0.15.5",
44
+ "@jay-framework/wix-cart": "^0.15.5",
45
+ "@jay-framework/wix-server-client": "^0.15.5",
46
+ "@jay-framework/wix-utils": "^0.15.5",
46
47
  "@wix/categories": "^1.0.185",
47
48
  "@wix/sdk": "^1.21.5",
48
- "@wix/stores": "^1.0.723",
49
+ "@wix/sdk-runtime": "^1.0.11",
50
+ "@wix/stores": "^1.0.742",
49
51
  "js-yaml": "^4.1.0"
50
52
  },
51
53
  "devDependencies": {
52
54
  "@babel/core": "^7.23.7",
53
55
  "@babel/preset-env": "^7.23.8",
54
56
  "@babel/preset-typescript": "^7.23.3",
55
- "@jay-framework/compiler-jay-stack": "^0.15.4",
56
- "@jay-framework/jay-cli": "^0.15.4",
57
- "@jay-framework/vite-plugin": "^0.15.4",
57
+ "@jay-framework/compiler-jay-stack": "^0.15.5",
58
+ "@jay-framework/jay-cli": "^0.15.5",
59
+ "@jay-framework/vite-plugin": "^0.15.5",
58
60
  "nodemon": "^3.0.3",
59
61
  "rimraf": "^5.0.5",
60
62
  "tslib": "^2.6.2",
package/plugin.yaml CHANGED
@@ -24,6 +24,8 @@ actions:
24
24
  action: get-product-by-slug.jay-action
25
25
  - name: getCategories
26
26
  action: get-categories.jay-action
27
+ - name: getVariantStock
28
+ action: get-variant-stock.jay-action
27
29
 
28
30
  # Plugin initialization uses makeJayInit pattern in lib/init.ts
29
31
  # Export name defaults to 'init'