@wix/headless-stores 0.0.57 → 0.0.59

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.
Files changed (37) hide show
  1. package/cjs/dist/react/ProductList.d.ts +0 -2
  2. package/cjs/dist/react/ProductList.js +3 -2
  3. package/cjs/dist/react/ProductListSort.d.ts +14 -0
  4. package/cjs/dist/react/ProductListSort.js +14 -0
  5. package/cjs/dist/react/core/ProductList.d.ts +3 -3
  6. package/cjs/dist/react/core/ProductList.js +3 -3
  7. package/cjs/dist/react/core/ProductListFilters.d.ts +8 -180
  8. package/cjs/dist/react/core/ProductListFilters.js +137 -171
  9. package/cjs/dist/react/core/ProductListPagination.d.ts +0 -192
  10. package/cjs/dist/react/core/ProductListPagination.js +2 -160
  11. package/cjs/dist/react/core/ProductListSort.d.ts +9 -57
  12. package/cjs/dist/react/core/ProductListSort.js +32 -52
  13. package/cjs/dist/services/index.d.ts +1 -2
  14. package/cjs/dist/services/index.js +1 -2
  15. package/cjs/dist/services/products-list-search-service.d.ts +1 -287
  16. package/cjs/dist/services/products-list-search-service.js +1 -900
  17. package/cjs/dist/services/products-list-service.d.ts +150 -4
  18. package/cjs/dist/services/products-list-service.js +537 -4
  19. package/dist/react/ProductList.d.ts +0 -2
  20. package/dist/react/ProductList.js +3 -2
  21. package/dist/react/ProductListSort.d.ts +14 -0
  22. package/dist/react/ProductListSort.js +14 -0
  23. package/dist/react/core/ProductList.d.ts +3 -3
  24. package/dist/react/core/ProductList.js +3 -3
  25. package/dist/react/core/ProductListFilters.d.ts +8 -180
  26. package/dist/react/core/ProductListFilters.js +137 -171
  27. package/dist/react/core/ProductListPagination.d.ts +0 -192
  28. package/dist/react/core/ProductListPagination.js +2 -160
  29. package/dist/react/core/ProductListSort.d.ts +9 -57
  30. package/dist/react/core/ProductListSort.js +32 -52
  31. package/dist/services/index.d.ts +1 -2
  32. package/dist/services/index.js +1 -2
  33. package/dist/services/products-list-search-service.d.ts +0 -287
  34. package/dist/services/products-list-search-service.js +1 -900
  35. package/dist/services/products-list-service.d.ts +150 -4
  36. package/dist/services/products-list-service.js +537 -4
  37. package/package.json +5 -4
@@ -1,9 +1,15 @@
1
1
  import { defineService, implementService } from '@wix/services-definitions';
2
2
  import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
3
- import { productsV3, readOnlyVariantsV3 } from '@wix/stores';
3
+ import { customizationsV3, productsV3, readOnlyVariantsV3 } from '@wix/stores';
4
4
  import { loadCategoriesListServiceConfig } from './categories-list-service.js';
5
- import { parseUrlToSearchOptions, } from './products-list-search-service.js';
6
5
  export const DEFAULT_QUERY_LIMIT = 100;
6
+ import { SortType } from './../enums/sort-enums.js';
7
+ export { SortType } from './../enums/sort-enums.js';
8
+ /**
9
+ * Enumeration of inventory status types available for filtering.
10
+ * Re-exports the Wix inventory availability status enum values.
11
+ */
12
+ export const InventoryStatusType = productsV3.InventoryAvailabilityStatus;
7
13
  /**
8
14
  * Loads products list service configuration from the Wix Stores API for SSR initialization.
9
15
  * This function is designed to be used during Server-Side Rendering (SSR) to preload
@@ -118,10 +124,13 @@ export const DEFAULT_QUERY_LIMIT = 100;
118
124
  */
119
125
  export async function loadProductsListServiceConfig(input) {
120
126
  let searchOptions;
127
+ const { items: customizations = [] } = await customizationsV3
128
+ .queryCustomizations()
129
+ .find();
121
130
  if (typeof input === 'string') {
122
131
  // URL input - parse it
123
132
  const categoriesListConfig = await loadCategoriesListServiceConfig();
124
- const { searchOptions: parsedOptions } = await parseUrlToSearchOptions(input, categoriesListConfig.categories);
133
+ const { searchOptions: parsedOptions } = await parseUrlToSearchOptions(input, categoriesListConfig.categories, customizations);
125
134
  searchOptions = parsedOptions;
126
135
  }
127
136
  else {
@@ -139,6 +148,7 @@ export async function loadProductsListServiceConfig(input) {
139
148
  searchOptions,
140
149
  pagingMetadata: resultWithFilter.pagingMetadata,
141
150
  aggregations: resultWithoutFilter.aggregationData ?? {},
151
+ customizations,
142
152
  };
143
153
  }
144
154
  /**
@@ -292,6 +302,14 @@ export const ProductListService = implementService.withConfig()(ProductsListServ
292
302
  const productsSignal = signalsService.signal(config.products);
293
303
  const searchOptionsSignal = signalsService.signal(config.searchOptions);
294
304
  const pagingMetadataSignal = signalsService.signal(config.pagingMetadata);
305
+ const minPriceSignal = signalsService.signal(getMinPrice(config.aggregations.results));
306
+ const maxPriceSignal = signalsService.signal(getMaxPrice(config.aggregations.results));
307
+ const availableProductOptionsSignal = signalsService.signal(getAvailableProductOptions(config.aggregations.results, config.customizations));
308
+ const availableInventoryStatusesSignal = signalsService.signal([
309
+ InventoryStatusType.IN_STOCK,
310
+ InventoryStatusType.OUT_OF_STOCK,
311
+ InventoryStatusType.PARTIALLY_OUT_OF_STOCK,
312
+ ]);
295
313
  const aggregationsSignal = signalsService.signal(config.aggregations);
296
314
  const isLoadingSignal = signalsService.signal(false);
297
315
  const errorSignal = signalsService.signal(null);
@@ -331,7 +349,15 @@ export const ProductListService = implementService.withConfig()(ProductsListServ
331
349
  searchOptions: searchOptionsSignal,
332
350
  pagingMetadata: pagingMetadataSignal,
333
351
  aggregations: aggregationsSignal,
334
- setSearchOptions: (searchOptions) => searchOptionsSignal.set(searchOptions),
352
+ /* Metadata for products list */
353
+ minPrice: minPriceSignal,
354
+ maxPrice: maxPriceSignal,
355
+ availableInventoryStatuses: availableInventoryStatusesSignal,
356
+ availableProductOptions: availableProductOptionsSignal,
357
+ /* End of Metadata for products list */
358
+ setSearchOptions: (searchOptions) => {
359
+ searchOptionsSignal.set(searchOptions);
360
+ },
335
361
  setSort: (sort) => {
336
362
  const currentOptions = searchOptionsSignal.peek();
337
363
  searchOptionsSignal.set({
@@ -346,7 +372,514 @@ export const ProductListService = implementService.withConfig()(ProductsListServ
346
372
  filter,
347
373
  });
348
374
  },
375
+ resetFilter: () => {
376
+ const currentOptions = searchOptionsSignal.peek();
377
+ searchOptionsSignal.set({
378
+ ...currentOptions,
379
+ filter: {},
380
+ });
381
+ },
382
+ isFiltered: () => {
383
+ return signalsService.computed(() => {
384
+ const currentOptions = searchOptionsSignal.peek();
385
+ if (!currentOptions.filter)
386
+ return false;
387
+ return (currentOptions.filter !== undefined &&
388
+ Object.keys(currentOptions.filter).length > 0);
389
+ });
390
+ },
349
391
  isLoading: isLoadingSignal,
350
392
  error: errorSignal,
393
+ loadMore: (count) => {
394
+ const currentOptions = searchOptionsSignal.peek();
395
+ searchOptionsSignal.set({
396
+ ...currentOptions,
397
+ cursorPaging: {
398
+ cursor: pagingMetadataSignal.get().cursors?.next,
399
+ limit: currentOptions.cursorPaging?.limit ?? 0 + count,
400
+ },
401
+ });
402
+ },
403
+ hasMoreProducts: signalsService.computed(() => pagingMetadataSignal.get().hasNext ?? false),
351
404
  };
352
405
  });
406
+ function getMinPrice(aggregationData) {
407
+ const minPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.minValue.amount');
408
+ if (minPriceAggregation?.scalar?.value) {
409
+ return Number(minPriceAggregation.scalar.value) || 0;
410
+ }
411
+ return 0;
412
+ }
413
+ function getMaxPrice(aggregationData) {
414
+ const maxPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.maxValue.amount');
415
+ if (maxPriceAggregation?.scalar?.value) {
416
+ return Number(maxPriceAggregation.scalar.value) || 0;
417
+ }
418
+ return 0;
419
+ }
420
+ function getAvailableProductOptions(aggregationData = [], customizations = []) {
421
+ const matchesAggregationName = (name, aggregationNames) => {
422
+ return aggregationNames.some((aggName) => aggName.toLowerCase() === name.toLowerCase());
423
+ };
424
+ const sortChoicesIntelligently = (choices) => {
425
+ return [...choices].sort((a, b) => {
426
+ const aIsNumber = /^\d+$/.test(a.name);
427
+ const bIsNumber = /^\d+$/.test(b.name);
428
+ if (aIsNumber && bIsNumber) {
429
+ return parseInt(a.name) - parseInt(b.name);
430
+ }
431
+ if (aIsNumber && !bIsNumber)
432
+ return -1;
433
+ if (!aIsNumber && bIsNumber)
434
+ return 1;
435
+ return a.name.localeCompare(b.name);
436
+ });
437
+ };
438
+ const optionNames = [];
439
+ const choiceNames = [];
440
+ aggregationData.forEach((result) => {
441
+ if (result.name === 'optionNames' && result.values?.results) {
442
+ optionNames.push(...result.values.results
443
+ .map((item) => item.value)
444
+ .filter((value) => typeof value === 'string'));
445
+ }
446
+ if (result.name === 'choiceNames' && result.values?.results) {
447
+ choiceNames.push(...result.values.results
448
+ .map((item) => item.value)
449
+ .filter((value) => typeof value === 'string'));
450
+ }
451
+ });
452
+ const options = customizations
453
+ .filter((customization) => customization.name &&
454
+ customization._id &&
455
+ customization.customizationType ===
456
+ customizationsV3.CustomizationType.PRODUCT_OPTION &&
457
+ (optionNames.length === 0 ||
458
+ matchesAggregationName(customization.name, optionNames)))
459
+ .map((customization) => {
460
+ const choices = (customization.choicesSettings?.choices || [])
461
+ .filter((choice) => choice._id &&
462
+ choice.name &&
463
+ (choiceNames.length === 0 ||
464
+ matchesAggregationName(choice.name, choiceNames)))
465
+ .map((choice) => ({
466
+ id: choice._id,
467
+ name: choice.name,
468
+ colorCode: choice.colorCode,
469
+ }));
470
+ return {
471
+ id: customization._id,
472
+ name: customization.name,
473
+ choices: sortChoicesIntelligently(choices),
474
+ optionRenderType: customization.customizationRenderType,
475
+ };
476
+ })
477
+ .filter((option) => option.choices.length > 0);
478
+ return options;
479
+ }
480
+ /**
481
+ * Parse URL and build complete search options with all filters, sort, and pagination.
482
+ * This function extracts search parameters, filters, sorting, and pagination from a URL
483
+ * and converts them into the format expected by the Wix Stores API.
484
+ *
485
+ * @param {string} url - The URL to parse search parameters from
486
+ * @param {Category[]} categoriesList - List of available categories for category slug resolution
487
+ * @param {productsV3.V3ProductSearch} [defaultSearchOptions] - Default search options to merge with parsed URL parameters
488
+ * @returns {Promise<{searchOptions: productsV3.V3ProductSearch, initialSearchState: InitialSearchState}>}
489
+ * Object containing both API-ready search options and UI-ready initial state
490
+ *
491
+ * @example
492
+ * ```tsx
493
+ * // Parse URL with filters, sort, and pagination
494
+ * const categories = await loadCategoriesListServiceConfig();
495
+ * const { searchOptions, initialSearchState } = await parseUrlToSearchOptions(
496
+ * 'https://example.com/products?sort=price:desc&Color=red,blue&minPrice=50',
497
+ * categories.categories
498
+ * );
499
+ *
500
+ * // Use searchOptions for API calls
501
+ * const products = await productsV3.searchProducts(searchOptions);
502
+ *
503
+ * // Use initialSearchState for UI initialization
504
+ * const filterState = initialSearchState.productOptions; // { colorId: ['red-id', 'blue-id'] }
505
+ * ```
506
+ */
507
+ export async function parseUrlToSearchOptions(url, categoriesList, customizations, defaultSearchOptions) {
508
+ const urlObj = new URL(url);
509
+ const searchParams = urlObj.searchParams;
510
+ // Build search options
511
+ const searchOptions = {
512
+ cursorPaging: {
513
+ limit: DEFAULT_QUERY_LIMIT,
514
+ },
515
+ ...defaultSearchOptions,
516
+ };
517
+ // Initialize search state for service
518
+ const initialSearchState = {};
519
+ // Extract category slug from URL path
520
+ // The category slug is always the last segment of the path
521
+ const pathSegments = urlObj.pathname.split('/').filter(Boolean);
522
+ let category = undefined;
523
+ if (pathSegments.length > 0) {
524
+ const lastSegment = pathSegments[pathSegments.length - 1];
525
+ // Check if the last segment matches any category in the categories list
526
+ category = categoriesList.find((cat) => cat.slug === lastSegment);
527
+ if (category) {
528
+ initialSearchState.category = category;
529
+ }
530
+ }
531
+ // Handle text search (q parameter)
532
+ const query = searchParams.get('q');
533
+ if (query) {
534
+ searchOptions.search = {
535
+ expression: query,
536
+ };
537
+ }
538
+ // Handle sorting
539
+ const sort = searchParams.get('sort');
540
+ if (sort) {
541
+ const sortType = convertUrlSortToSortType(sort);
542
+ if (sortType) {
543
+ initialSearchState.sort = sortType;
544
+ // Apply sort to search options
545
+ switch (sortType) {
546
+ case SortType.NAME_ASC:
547
+ searchOptions.sort = [
548
+ { fieldName: 'name', order: productsV3.SortDirection.ASC },
549
+ ];
550
+ break;
551
+ case SortType.NAME_DESC:
552
+ searchOptions.sort = [
553
+ { fieldName: 'name', order: productsV3.SortDirection.DESC },
554
+ ];
555
+ break;
556
+ case SortType.PRICE_ASC:
557
+ searchOptions.sort = [
558
+ {
559
+ fieldName: 'actualPriceRange.minValue.amount',
560
+ order: productsV3.SortDirection.ASC,
561
+ },
562
+ ];
563
+ break;
564
+ case SortType.PRICE_DESC:
565
+ searchOptions.sort = [
566
+ {
567
+ fieldName: 'actualPriceRange.minValue.amount',
568
+ order: productsV3.SortDirection.DESC,
569
+ },
570
+ ];
571
+ break;
572
+ case SortType.RECOMMENDED:
573
+ searchOptions.sort = [
574
+ {
575
+ fieldName: 'name',
576
+ order: productsV3.SortDirection.DESC,
577
+ },
578
+ ];
579
+ break;
580
+ }
581
+ }
582
+ }
583
+ // Handle pagination
584
+ const limit = searchParams.get('limit');
585
+ const cursor = searchParams.get('cursor');
586
+ if (limit || cursor) {
587
+ searchOptions.cursorPaging = {};
588
+ if (limit) {
589
+ const limitNum = parseInt(limit, 10);
590
+ if (!isNaN(limitNum) && limitNum > 0) {
591
+ searchOptions.cursorPaging.limit = limitNum;
592
+ initialSearchState.limit = limitNum;
593
+ }
594
+ }
595
+ if (cursor) {
596
+ searchOptions.cursorPaging.cursor = cursor;
597
+ initialSearchState.cursor = cursor;
598
+ }
599
+ }
600
+ // Handle filtering for search options
601
+ const filter = {};
602
+ const visible = searchParams.get('visible');
603
+ if (visible !== null) {
604
+ filter['visible'] = visible === 'true';
605
+ initialSearchState.visible = visible === 'true';
606
+ }
607
+ const productType = searchParams.get('productType');
608
+ if (productType) {
609
+ filter['productType'] = productType;
610
+ initialSearchState.productType = productType;
611
+ }
612
+ // Add category filter if found
613
+ if (category) {
614
+ filter['allCategoriesInfo.categories'] = {
615
+ $matchItems: [{ _id: { $in: [category._id] } }],
616
+ };
617
+ }
618
+ // Price range filtering
619
+ const minPrice = searchParams.get('minPrice');
620
+ const maxPrice = searchParams.get('maxPrice');
621
+ if (minPrice || maxPrice) {
622
+ initialSearchState.priceRange = {};
623
+ if (minPrice) {
624
+ const minPriceNum = parseFloat(minPrice);
625
+ if (!isNaN(minPriceNum)) {
626
+ filter['actualPriceRange.minValue.amount'] = { $gte: minPriceNum };
627
+ initialSearchState.priceRange.min = minPriceNum;
628
+ }
629
+ }
630
+ if (maxPrice) {
631
+ const maxPriceNum = parseFloat(maxPrice);
632
+ if (!isNaN(maxPriceNum)) {
633
+ filter['actualPriceRange.maxValue.amount'] = { $lte: maxPriceNum };
634
+ initialSearchState.priceRange.max = maxPriceNum;
635
+ }
636
+ }
637
+ }
638
+ const inventoryStatus = searchParams.get('inventoryStatus');
639
+ if (inventoryStatus) {
640
+ filter['inventory.availabilityStatus'] = {
641
+ $in: inventoryStatus.split(','),
642
+ };
643
+ }
644
+ // Parse product options from URL parameters
645
+ const reservedParams = [
646
+ 'minPrice',
647
+ 'maxPrice',
648
+ 'inventory_status',
649
+ 'inventoryStatus',
650
+ 'visible',
651
+ 'productType',
652
+ 'q',
653
+ 'limit',
654
+ 'cursor',
655
+ 'sort',
656
+ ];
657
+ const productOptionsById = {};
658
+ for (const [optionName, optionValues] of searchParams.entries()) {
659
+ if (reservedParams.includes(optionName))
660
+ continue;
661
+ // Find the option by name in customizations
662
+ const option = customizations.find((c) => c.name === optionName &&
663
+ c.customizationType ===
664
+ customizationsV3.CustomizationType.PRODUCT_OPTION);
665
+ if (option && option._id) {
666
+ const choiceValues = optionValues.split(',').filter(Boolean);
667
+ const choiceIds = [];
668
+ // Convert choice names to IDs
669
+ for (const choiceName of choiceValues) {
670
+ const choice = option.choicesSettings?.choices?.find((c) => c.name === choiceName);
671
+ if (choice && choice._id) {
672
+ choiceIds.push(choice._id);
673
+ }
674
+ }
675
+ if (choiceIds.length > 0) {
676
+ productOptionsById[option._id] = choiceIds;
677
+ }
678
+ }
679
+ }
680
+ if (Object.keys(productOptionsById).length > 0) {
681
+ initialSearchState.productOptions = productOptionsById;
682
+ // Add product option filter to search options
683
+ filter[`options.choicesSettings.choices.choiceId`] = {
684
+ $hasSome: Object.values(productOptionsById).flat(),
685
+ };
686
+ }
687
+ // Add filter to search options if any filters were set
688
+ if (Object.keys(filter).length > 0) {
689
+ searchOptions.filter = filter;
690
+ }
691
+ // Add aggregations for getting filter options
692
+ searchOptions.aggregations = [
693
+ {
694
+ name: 'minPrice',
695
+ fieldPath: 'actualPriceRange.minValue.amount',
696
+ type: 'SCALAR',
697
+ scalar: { type: 'MIN' },
698
+ },
699
+ {
700
+ name: 'maxPrice',
701
+ fieldPath: 'actualPriceRange.maxValue.amount',
702
+ type: 'SCALAR',
703
+ scalar: { type: 'MAX' },
704
+ },
705
+ {
706
+ name: 'optionNames',
707
+ fieldPath: 'options.name',
708
+ type: productsV3.SortType.VALUE,
709
+ value: {
710
+ limit: 20,
711
+ sortType: productsV3.SortType.VALUE,
712
+ sortDirection: productsV3.SortDirection.ASC,
713
+ },
714
+ },
715
+ {
716
+ name: 'choiceNames',
717
+ fieldPath: 'options.choicesSettings.choices.name',
718
+ type: productsV3.SortType.VALUE,
719
+ value: {
720
+ limit: 50,
721
+ sortType: productsV3.SortType.VALUE,
722
+ sortDirection: productsV3.SortDirection.ASC,
723
+ },
724
+ },
725
+ {
726
+ name: 'inventoryStatus',
727
+ fieldPath: 'inventory.availabilityStatus',
728
+ type: productsV3.SortType.VALUE,
729
+ value: {
730
+ limit: 10,
731
+ sortType: productsV3.SortType.VALUE,
732
+ sortDirection: productsV3.SortDirection.ASC,
733
+ },
734
+ },
735
+ ];
736
+ return { searchOptions, initialSearchState };
737
+ }
738
+ /**
739
+ * Convert SortType enum to URL format
740
+ */
741
+ function convertSortTypeToUrl(sortType) {
742
+ switch (sortType) {
743
+ case SortType.NAME_ASC:
744
+ return 'name';
745
+ case SortType.NAME_DESC:
746
+ return 'name:desc';
747
+ case SortType.PRICE_ASC:
748
+ return 'price';
749
+ case SortType.PRICE_DESC:
750
+ return 'price:desc';
751
+ case SortType.NEWEST:
752
+ return 'newest';
753
+ case SortType.RECOMMENDED:
754
+ return 'recommended';
755
+ default:
756
+ return 'name';
757
+ }
758
+ }
759
+ /**
760
+ * Convert URL sort format to SortType enum
761
+ */
762
+ export function convertUrlSortToSortType(urlSort) {
763
+ const sortParts = urlSort.split(':');
764
+ const field = sortParts[0]?.toLowerCase();
765
+ const order = sortParts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc';
766
+ switch (field) {
767
+ case 'name':
768
+ return order === 'desc' ? SortType.NAME_DESC : SortType.NAME_ASC;
769
+ case 'price':
770
+ return order === 'desc' ? SortType.PRICE_DESC : SortType.PRICE_ASC;
771
+ case 'newest':
772
+ case 'created':
773
+ return SortType.NEWEST;
774
+ case 'recommended':
775
+ return SortType.RECOMMENDED;
776
+ default:
777
+ return null;
778
+ }
779
+ }
780
+ /**
781
+ * Update URL with current search state (sort, filters, pagination)
782
+ */
783
+ //@ts-expect-error
784
+ function updateUrlWithSearchState(searchState) {
785
+ if (typeof window === 'undefined')
786
+ return;
787
+ const { sort, filters, customizations, catalogBounds, categorySlug } = searchState;
788
+ // Convert filter IDs back to human-readable names for URL
789
+ const humanReadableOptions = {};
790
+ for (const [optionId, choiceIds] of Object.entries(filters?.productOptions ?? {})) {
791
+ const option = customizations.find((c) => c._id === optionId);
792
+ if (option && option.name) {
793
+ const choiceNames = [];
794
+ for (const choiceId of choiceIds) {
795
+ const choice = option.choicesSettings?.choices?.find((c) => c._id === choiceId);
796
+ if (choice && choice.name) {
797
+ choiceNames.push(choice.name);
798
+ }
799
+ }
800
+ if (choiceNames.length > 0) {
801
+ humanReadableOptions[option.name] = choiceNames;
802
+ }
803
+ }
804
+ }
805
+ // Start with current URL parameters to preserve non-search parameters
806
+ const params = new URLSearchParams(window.location.search);
807
+ // Define search-related parameters that we manage
808
+ const searchParams = [
809
+ 'sort',
810
+ 'limit',
811
+ 'cursor',
812
+ 'minPrice',
813
+ 'maxPrice',
814
+ 'inventoryStatus',
815
+ 'visible',
816
+ 'productType',
817
+ // Product option names will be dynamically added below
818
+ // Note: category is NOT included here as it's handled in the URL path
819
+ ];
820
+ // Remove existing search parameters first
821
+ searchParams.forEach((param) => params.delete(param));
822
+ // Remove existing product option parameters (they have dynamic names)
823
+ for (const customization of customizations) {
824
+ if (customization.customizationType ===
825
+ customizationsV3.CustomizationType.PRODUCT_OPTION &&
826
+ customization.name) {
827
+ params.delete(customization.name);
828
+ }
829
+ }
830
+ // Add sort parameter (only if not default)
831
+ const urlSort = convertSortTypeToUrl(sort);
832
+ if (sort !== SortType.NAME_ASC) {
833
+ params.set('sort', urlSort);
834
+ }
835
+ // Add price range parameters only if they differ from catalog bounds
836
+ if (filters.priceRange?.min &&
837
+ filters.priceRange.min > catalogBounds.minPrice) {
838
+ params.set('minPrice', filters.priceRange.min.toString());
839
+ }
840
+ if (filters.priceRange?.max &&
841
+ filters.priceRange.max < catalogBounds.maxPrice) {
842
+ params.set('maxPrice', filters.priceRange.max.toString());
843
+ }
844
+ // Add inventory status parameters
845
+ if (filters.inventoryStatuses && filters.inventoryStatuses.length > 0) {
846
+ params.set('inventoryStatus', filters.inventoryStatuses.join(','));
847
+ }
848
+ // Add visibility filter (only if explicitly false, since true is default)
849
+ if (filters.visible === false) {
850
+ params.set('visible', 'false');
851
+ }
852
+ // Add product type filter
853
+ if (filters.productType) {
854
+ params.set('productType', filters.productType);
855
+ }
856
+ // Add product options as individual parameters (Color=Red,Blue&Size=Large)
857
+ for (const [optionName, values] of Object.entries(humanReadableOptions)) {
858
+ if (values.length > 0) {
859
+ params.set(optionName, values.join(','));
860
+ }
861
+ }
862
+ // Handle URL path construction with category
863
+ let baseUrl = window.location.pathname;
864
+ // If categorySlug is provided, replace the last path segment (which represents the category)
865
+ if (categorySlug) {
866
+ const pathSegments = baseUrl.split('/').filter(Boolean);
867
+ if (pathSegments.length > 0) {
868
+ // Replace the last segment with the new category slug
869
+ pathSegments[pathSegments.length - 1] = categorySlug;
870
+ baseUrl = '/' + pathSegments.join('/');
871
+ }
872
+ else {
873
+ // If no segments, just use the category slug
874
+ baseUrl = `/${categorySlug}`;
875
+ }
876
+ }
877
+ // Build the new URL
878
+ const newUrl = params.toString()
879
+ ? `${baseUrl}?${params.toString()}`
880
+ : baseUrl;
881
+ // Only update if URL actually changed
882
+ if (newUrl !== window.location.pathname + window.location.search) {
883
+ window.history.pushState(null, '', newUrl);
884
+ }
885
+ }
@@ -1,6 +1,5 @@
1
1
  import type { V3Product } from '@wix/auto_sdk_stores_products-v-3';
2
2
  import React from 'react';
3
- import type { ProductsListSearchServiceConfig } from '../services/products-list-search-service.js';
4
3
  import type { ProductsListServiceConfig } from '../services/products-list-service.js';
5
4
  /**
6
5
  * Props for the ProductList root component following the documented API
@@ -9,7 +8,6 @@ export interface ProductListRootProps {
9
8
  children: React.ReactNode;
10
9
  products?: V3Product[];
11
10
  productsListConfig?: ProductsListServiceConfig;
12
- productsListSearchConfig?: ProductsListSearchServiceConfig;
13
11
  className?: string;
14
12
  }
15
13
  /**
@@ -38,7 +38,7 @@ var TestIds;
38
38
  * ```
39
39
  */
40
40
  export const Root = React.forwardRef((props, ref) => {
41
- const { children, products, productsListConfig, productsListSearchConfig, className, } = props;
41
+ const { children, products, productsListConfig, className } = props;
42
42
  const serviceConfig = productsListConfig || {
43
43
  products: products || [],
44
44
  searchOptions: {
@@ -48,8 +48,9 @@ export const Root = React.forwardRef((props, ref) => {
48
48
  count: products?.length || 0,
49
49
  },
50
50
  aggregations: {}, // Empty aggregation data
51
+ customizations: [],
51
52
  };
52
- return (_jsx(CoreProductList.Root, { productsListConfig: serviceConfig, productsListSearchConfig: productsListSearchConfig, children: _jsx(RootContent, { children: children, className: className, ref: ref }) }));
53
+ return (_jsx(CoreProductList.Root, { productsListConfig: serviceConfig, children: _jsx(RootContent, { children: children, className: className, ref: ref }) }));
53
54
  });
54
55
  /**
55
56
  * Internal component to handle the Root content with service access
@@ -0,0 +1,14 @@
1
+ import { Sort as SortPrimitive } from '@wix/headless-components/react';
2
+ import { productsV3 } from '@wix/stores';
3
+ import React from 'react';
4
+ export interface ProductListSortProps {
5
+ children?: (props: {
6
+ currentSort: productsV3.V3ProductSearch['sort'];
7
+ sortOptions: SortPrimitive.SortOption[];
8
+ setSort: (sort: productsV3.V3ProductSearch['sort']) => void;
9
+ }) => React.ReactNode;
10
+ className?: string;
11
+ as?: 'select' | 'list';
12
+ asChild?: boolean;
13
+ }
14
+ export declare const ProductListSort: React.ForwardRefExoticComponent<ProductListSortProps & React.RefAttributes<HTMLElement>>;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Sort as SortPrimitive } from '@wix/headless-components/react';
3
+ import { ProductListSort as ProductListSortPrimitive } from './core/ProductListSort.js';
4
+ import React from 'react';
5
+ export const ProductListSort = React.forwardRef(({ children, className, as, asChild }, ref) => {
6
+ return (_jsx(ProductListSortPrimitive, { children: ({ currentSort, sortOptions, setSort }) => {
7
+ if (asChild && children) {
8
+ return children({ currentSort, sortOptions, setSort });
9
+ }
10
+ return (_jsx(SortPrimitive.Root, { ref: ref, value: currentSort, onChange: (value) => {
11
+ setSort(value);
12
+ }, sortOptions: sortOptions, as: as, className: className }));
13
+ } }));
14
+ });
@@ -1,6 +1,6 @@
1
1
  import { type ProductsListServiceConfig } from '../../services/products-list-service.js';
2
2
  import { productsV3 } from '@wix/stores';
3
- import { ProductsListSearchServiceConfig } from '../../services/products-list-search-service.js';
3
+ import { CategoriesListServiceConfig } from '../../services/categories-list-service.js';
4
4
  /**
5
5
  * Props for Root headless component
6
6
  */
@@ -9,8 +9,8 @@ export interface RootProps {
9
9
  children: React.ReactNode;
10
10
  /** Configuration for the ProductList service */
11
11
  productsListConfig: ProductsListServiceConfig;
12
- /** Configuration for the ProductListSearch service */
13
- productsListSearchConfig?: ProductsListSearchServiceConfig;
12
+ /** Configuration for the CategoriesList service */
13
+ categoriesListConfig?: CategoriesListServiceConfig;
14
14
  }
15
15
  /**
16
16
  * Root component that provides both ProductList and ProductListSearch service contexts to its children.