@wix/headless-stores 0.0.41 → 0.0.42

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 (41) hide show
  1. package/cjs/dist/react/ProductList.d.ts +31 -3
  2. package/cjs/dist/react/ProductList.js +33 -5
  3. package/cjs/dist/react/ProductListFilters.d.ts +91 -111
  4. package/cjs/dist/react/ProductListFilters.js +98 -115
  5. package/cjs/dist/react/ProductListPagination.d.ts +89 -96
  6. package/cjs/dist/react/ProductListPagination.js +96 -104
  7. package/cjs/dist/react/ProductListSort.d.ts +26 -57
  8. package/cjs/dist/react/ProductListSort.js +26 -58
  9. package/cjs/dist/services/index.d.ts +1 -3
  10. package/cjs/dist/services/index.js +1 -3
  11. package/cjs/dist/services/products-list-search-service.d.ts +219 -0
  12. package/cjs/dist/services/products-list-search-service.js +794 -0
  13. package/cjs/dist/services/products-list-service.d.ts +28 -11
  14. package/cjs/dist/services/products-list-service.js +26 -6
  15. package/dist/react/ProductList.d.ts +31 -3
  16. package/dist/react/ProductList.js +33 -5
  17. package/dist/react/ProductListFilters.d.ts +91 -111
  18. package/dist/react/ProductListFilters.js +98 -115
  19. package/dist/react/ProductListPagination.d.ts +89 -96
  20. package/dist/react/ProductListPagination.js +96 -104
  21. package/dist/react/ProductListSort.d.ts +26 -57
  22. package/dist/react/ProductListSort.js +26 -58
  23. package/dist/services/index.d.ts +1 -3
  24. package/dist/services/index.js +1 -3
  25. package/dist/services/products-list-search-service.d.ts +219 -0
  26. package/dist/services/products-list-search-service.js +794 -0
  27. package/dist/services/products-list-service.d.ts +28 -11
  28. package/dist/services/products-list-service.js +26 -6
  29. package/package.json +2 -2
  30. package/cjs/dist/services/products-list-filters-service.d.ts +0 -309
  31. package/cjs/dist/services/products-list-filters-service.js +0 -504
  32. package/cjs/dist/services/products-list-pagination-service.d.ts +0 -186
  33. package/cjs/dist/services/products-list-pagination-service.js +0 -179
  34. package/cjs/dist/services/products-list-sort-service.d.ts +0 -117
  35. package/cjs/dist/services/products-list-sort-service.js +0 -144
  36. package/dist/services/products-list-filters-service.d.ts +0 -309
  37. package/dist/services/products-list-filters-service.js +0 -504
  38. package/dist/services/products-list-pagination-service.d.ts +0 -186
  39. package/dist/services/products-list-pagination-service.js +0 -179
  40. package/dist/services/products-list-sort-service.d.ts +0 -117
  41. package/dist/services/products-list-sort-service.js +0 -144
@@ -0,0 +1,794 @@
1
+ import { defineService, implementService } from "@wix/services-definitions";
2
+ import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals";
3
+ import { DEFAULT_QUERY_LIMIT, ProductsListServiceDefinition, } from "./products-list-service.js";
4
+ import { productsV3, customizationsV3 } from "@wix/stores";
5
+ const PRICE_FILTER_DEBOUNCE_TIME = 300;
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;
13
+ /**
14
+ * Service definition for the Products List Search service.
15
+ * This consolidates sort, pagination, and filtering functionality.
16
+ */
17
+ export const ProductsListSearchServiceDefinition = defineService("products-list-search");
18
+ /**
19
+ * Convert SortType enum to URL format
20
+ */
21
+ function convertSortTypeToUrl(sortType) {
22
+ switch (sortType) {
23
+ case SortType.NAME_ASC:
24
+ return "name";
25
+ case SortType.NAME_DESC:
26
+ return "name:desc";
27
+ case SortType.PRICE_ASC:
28
+ return "price";
29
+ case SortType.PRICE_DESC:
30
+ return "price:desc";
31
+ case SortType.NEWEST:
32
+ return "newest";
33
+ case SortType.RECOMMENDED:
34
+ return "recommended";
35
+ default:
36
+ return "name";
37
+ }
38
+ }
39
+ /**
40
+ * Convert URL sort format to SortType enum
41
+ */
42
+ export function convertUrlSortToSortType(urlSort) {
43
+ const sortParts = urlSort.split(":");
44
+ const field = sortParts[0]?.toLowerCase();
45
+ const order = sortParts[1]?.toLowerCase() === "desc" ? "desc" : "asc";
46
+ switch (field) {
47
+ case "name":
48
+ return order === "desc" ? SortType.NAME_DESC : SortType.NAME_ASC;
49
+ case "price":
50
+ return order === "desc" ? SortType.PRICE_DESC : SortType.PRICE_ASC;
51
+ case "newest":
52
+ case "created":
53
+ return SortType.NEWEST;
54
+ case "recommended":
55
+ return SortType.RECOMMENDED;
56
+ default:
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * Update URL with current search state (sort, filters, pagination)
62
+ */
63
+ function updateUrlWithSearchState(searchState) {
64
+ if (typeof window === "undefined")
65
+ return;
66
+ const { sort, filters, customizations, catalogBounds } = searchState;
67
+ // Convert filter IDs back to human-readable names for URL
68
+ const humanReadableOptions = {};
69
+ for (const [optionId, choiceIds] of Object.entries(filters?.productOptions ?? {})) {
70
+ const option = customizations.find((c) => c._id === optionId);
71
+ if (option && option.name) {
72
+ const choiceNames = [];
73
+ for (const choiceId of choiceIds) {
74
+ const choice = option.choicesSettings?.choices?.find((c) => c._id === choiceId);
75
+ if (choice && choice.name) {
76
+ choiceNames.push(choice.name);
77
+ }
78
+ }
79
+ if (choiceNames.length > 0) {
80
+ humanReadableOptions[option.name] = choiceNames;
81
+ }
82
+ }
83
+ }
84
+ // Start with current URL parameters to preserve non-search parameters
85
+ const params = new URLSearchParams(window.location.search);
86
+ // Define search-related parameters that we manage
87
+ const searchParams = [
88
+ "sort",
89
+ "limit",
90
+ "cursor",
91
+ "minPrice",
92
+ "maxPrice",
93
+ "inventoryStatus",
94
+ "category",
95
+ "visible",
96
+ "productType",
97
+ // Product option names will be dynamically added below
98
+ ];
99
+ // Remove existing search parameters first
100
+ searchParams.forEach((param) => params.delete(param));
101
+ // Remove existing product option parameters (they have dynamic names)
102
+ for (const customization of customizations) {
103
+ if (customization.customizationType ===
104
+ customizationsV3.CustomizationType.PRODUCT_OPTION &&
105
+ customization.name) {
106
+ params.delete(customization.name);
107
+ }
108
+ }
109
+ // Add sort parameter (only if not default)
110
+ const urlSort = convertSortTypeToUrl(sort);
111
+ if (sort !== SortType.NAME_ASC) {
112
+ params.set("sort", urlSort);
113
+ }
114
+ // Add price range parameters only if they differ from catalog bounds
115
+ if (filters.priceRange?.min &&
116
+ filters.priceRange.min > catalogBounds.minPrice) {
117
+ params.set("minPrice", filters.priceRange.min.toString());
118
+ }
119
+ if (filters.priceRange?.max &&
120
+ filters.priceRange.max < catalogBounds.maxPrice) {
121
+ params.set("maxPrice", filters.priceRange.max.toString());
122
+ }
123
+ // Add inventory status parameters
124
+ if (filters.inventoryStatuses && filters.inventoryStatuses.length > 0) {
125
+ params.set("inventoryStatus", filters.inventoryStatuses.join(","));
126
+ }
127
+ // Add category filter
128
+ if (filters.category) {
129
+ params.set("category", filters.category);
130
+ }
131
+ // Add visibility filter (only if explicitly false, since true is default)
132
+ if (filters.visible === false) {
133
+ params.set("visible", "false");
134
+ }
135
+ // Add product type filter
136
+ if (filters.productType) {
137
+ params.set("productType", filters.productType);
138
+ }
139
+ // Add product options as individual parameters (Color=Red,Blue&Size=Large)
140
+ for (const [optionName, values] of Object.entries(humanReadableOptions)) {
141
+ if (values.length > 0) {
142
+ params.set(optionName, values.join(","));
143
+ }
144
+ }
145
+ // Build the new URL
146
+ const baseUrl = window.location.pathname;
147
+ const newUrl = params.toString()
148
+ ? `${baseUrl}?${params.toString()}`
149
+ : baseUrl;
150
+ // Only update if URL actually changed
151
+ if (newUrl !== window.location.pathname + window.location.search) {
152
+ window.history.pushState(null, "", newUrl);
153
+ }
154
+ }
155
+ /**
156
+ * Parse URL and build complete search options with all filters, sort, and pagination
157
+ */
158
+ export async function parseUrlForProductsListSearch(url, defaultSearchOptions) {
159
+ const urlObj = new URL(url);
160
+ const searchParams = urlObj.searchParams;
161
+ // Get customizations for product option parsing
162
+ const { items: customizations = [] } = await customizationsV3
163
+ .queryCustomizations()
164
+ .find();
165
+ // Build search options
166
+ const searchOptions = {
167
+ cursorPaging: {
168
+ limit: DEFAULT_QUERY_LIMIT,
169
+ },
170
+ ...defaultSearchOptions,
171
+ };
172
+ // Initialize search state for service
173
+ const initialSearchState = {};
174
+ // Handle text search (q parameter)
175
+ const query = searchParams.get("q");
176
+ if (query) {
177
+ searchOptions.search = {
178
+ expression: query,
179
+ };
180
+ }
181
+ // Handle sorting
182
+ const sort = searchParams.get("sort");
183
+ if (sort) {
184
+ const sortType = convertUrlSortToSortType(sort);
185
+ if (sortType) {
186
+ initialSearchState.sort = sortType;
187
+ // Apply sort to search options
188
+ switch (sortType) {
189
+ case SortType.NAME_ASC:
190
+ searchOptions.sort = [
191
+ { fieldName: "name", order: productsV3.SortDirection.ASC },
192
+ ];
193
+ break;
194
+ case SortType.NAME_DESC:
195
+ searchOptions.sort = [
196
+ { fieldName: "name", order: productsV3.SortDirection.DESC },
197
+ ];
198
+ break;
199
+ case SortType.PRICE_ASC:
200
+ searchOptions.sort = [
201
+ {
202
+ fieldName: "actualPriceRange.minValue.amount",
203
+ order: productsV3.SortDirection.ASC,
204
+ },
205
+ ];
206
+ break;
207
+ case SortType.PRICE_DESC:
208
+ searchOptions.sort = [
209
+ {
210
+ fieldName: "actualPriceRange.minValue.amount",
211
+ order: productsV3.SortDirection.DESC,
212
+ },
213
+ ];
214
+ break;
215
+ case SortType.RECOMMENDED:
216
+ searchOptions.sort = [
217
+ {
218
+ fieldName: "name",
219
+ order: productsV3.SortDirection.DESC,
220
+ },
221
+ ];
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ // Handle pagination
227
+ const limit = searchParams.get("limit");
228
+ const cursor = searchParams.get("cursor");
229
+ if (limit || cursor) {
230
+ searchOptions.cursorPaging = {};
231
+ if (limit) {
232
+ const limitNum = parseInt(limit, 10);
233
+ if (!isNaN(limitNum) && limitNum > 0) {
234
+ searchOptions.cursorPaging.limit = limitNum;
235
+ initialSearchState.limit = limitNum;
236
+ }
237
+ }
238
+ if (cursor) {
239
+ searchOptions.cursorPaging.cursor = cursor;
240
+ initialSearchState.cursor = cursor;
241
+ }
242
+ }
243
+ // Handle filtering for search options
244
+ const filter = {};
245
+ const visible = searchParams.get("visible");
246
+ if (visible !== null) {
247
+ filter["visible"] = visible === "true";
248
+ initialSearchState.visible = visible === "true";
249
+ }
250
+ const productType = searchParams.get("productType");
251
+ if (productType) {
252
+ filter["productType"] = productType;
253
+ initialSearchState.productType = productType;
254
+ }
255
+ const category = searchParams.get("category");
256
+ if (category) {
257
+ filter["allCategoriesInfo.categories"] = {
258
+ $matchItems: [{ _id: { $in: [category] } }],
259
+ };
260
+ initialSearchState.category = category;
261
+ }
262
+ // Price range filtering
263
+ const minPrice = searchParams.get("minPrice");
264
+ const maxPrice = searchParams.get("maxPrice");
265
+ if (minPrice || maxPrice) {
266
+ initialSearchState.priceRange = {};
267
+ if (minPrice) {
268
+ const minPriceNum = parseFloat(minPrice);
269
+ if (!isNaN(minPriceNum)) {
270
+ filter["actualPriceRange.minValue.amount"] = { $gte: minPriceNum };
271
+ initialSearchState.priceRange.min = minPriceNum;
272
+ }
273
+ }
274
+ if (maxPrice) {
275
+ const maxPriceNum = parseFloat(maxPrice);
276
+ if (!isNaN(maxPriceNum)) {
277
+ filter["actualPriceRange.maxValue.amount"] = { $lte: maxPriceNum };
278
+ initialSearchState.priceRange.max = maxPriceNum;
279
+ }
280
+ }
281
+ }
282
+ // Parse product options from URL parameters
283
+ const reservedParams = [
284
+ "minPrice",
285
+ "maxPrice",
286
+ "inventory_status",
287
+ "inventoryStatus",
288
+ "category",
289
+ "visible",
290
+ "productType",
291
+ "q",
292
+ "limit",
293
+ "cursor",
294
+ "sort",
295
+ ];
296
+ const productOptionsById = {};
297
+ for (const [optionName, optionValues] of searchParams.entries()) {
298
+ if (reservedParams.includes(optionName))
299
+ continue;
300
+ // Find the option by name in customizations
301
+ const option = customizations.find((c) => c.name === optionName &&
302
+ c.customizationType ===
303
+ customizationsV3.CustomizationType.PRODUCT_OPTION);
304
+ if (option && option._id) {
305
+ const choiceValues = optionValues.split(",").filter(Boolean);
306
+ const choiceIds = [];
307
+ // Convert choice names to IDs
308
+ for (const choiceName of choiceValues) {
309
+ const choice = option.choicesSettings?.choices?.find((c) => c.name === choiceName);
310
+ if (choice && choice._id) {
311
+ choiceIds.push(choice._id);
312
+ }
313
+ }
314
+ if (choiceIds.length > 0) {
315
+ productOptionsById[option._id] = choiceIds;
316
+ // Add product option filter to search options
317
+ filter[`options.choicesSettings.choices.choiceId`] = {
318
+ $hasSome: choiceIds,
319
+ };
320
+ }
321
+ }
322
+ }
323
+ if (Object.keys(productOptionsById).length > 0) {
324
+ initialSearchState.productOptions = productOptionsById;
325
+ }
326
+ // Add filter to search options if any filters were set
327
+ if (Object.keys(filter).length > 0) {
328
+ searchOptions.filter = filter;
329
+ }
330
+ // Add aggregations for getting filter options
331
+ searchOptions.aggregations = [
332
+ {
333
+ name: "minPrice",
334
+ fieldPath: "actualPriceRange.minValue.amount",
335
+ type: "SCALAR",
336
+ scalar: { type: "MIN" },
337
+ },
338
+ {
339
+ name: "maxPrice",
340
+ fieldPath: "actualPriceRange.maxValue.amount",
341
+ type: "SCALAR",
342
+ scalar: { type: "MAX" },
343
+ },
344
+ {
345
+ name: "optionNames",
346
+ fieldPath: "options.name",
347
+ type: productsV3.SortType.VALUE,
348
+ value: {
349
+ limit: 20,
350
+ sortType: productsV3.SortType.VALUE,
351
+ sortDirection: productsV3.SortDirection.ASC,
352
+ },
353
+ },
354
+ {
355
+ name: "choiceNames",
356
+ fieldPath: "options.choicesSettings.choices.name",
357
+ type: productsV3.SortType.VALUE,
358
+ value: {
359
+ limit: 50,
360
+ sortType: productsV3.SortType.VALUE,
361
+ sortDirection: productsV3.SortDirection.ASC,
362
+ },
363
+ },
364
+ {
365
+ name: "inventoryStatus",
366
+ fieldPath: "inventory.availabilityStatus",
367
+ type: productsV3.SortType.VALUE,
368
+ value: {
369
+ limit: 10,
370
+ sortType: productsV3.SortType.VALUE,
371
+ sortDirection: productsV3.SortDirection.ASC,
372
+ },
373
+ },
374
+ ];
375
+ return { searchOptions, initialSearchState };
376
+ }
377
+ /**
378
+ * Load search service configuration from URL
379
+ */
380
+ export async function loadProductsListSearchServiceConfig(url) {
381
+ const { initialSearchState } = await parseUrlForProductsListSearch(url);
382
+ const { items: customizations = [] } = await customizationsV3
383
+ .queryCustomizations()
384
+ .find();
385
+ return {
386
+ customizations,
387
+ initialSearchState,
388
+ };
389
+ }
390
+ /**
391
+ * Implementation of the Products List Search service
392
+ */
393
+ export const ProductsListSearchService = implementService.withConfig()(ProductsListSearchServiceDefinition, ({ getService, config }) => {
394
+ let firstRun = true;
395
+ const signalsService = getService(SignalsServiceDefinition);
396
+ const productsListService = getService(ProductsListServiceDefinition);
397
+ const { customizations, initialSearchState } = config;
398
+ const aggregationData = productsListService.aggregations.get()?.results;
399
+ const currentSearchOptions = productsListService.searchOptions.get();
400
+ // Sort signals
401
+ const selectedSortOptionSignal = signalsService.signal(initialSearchState?.sort || SortType.NAME_ASC);
402
+ // Pagination signals
403
+ const currentLimitSignal = signalsService.signal(initialSearchState?.limit || getCurrentLimit(currentSearchOptions));
404
+ const currentCursorSignal = signalsService.signal(initialSearchState?.cursor || getCurrentCursor(currentSearchOptions));
405
+ // Filter signals
406
+ const catalogPriceRange = getCatalogPriceRange(aggregationData || []);
407
+ const userFilterMinPriceSignal = signalsService.signal(initialSearchState?.priceRange?.min ?? catalogPriceRange.minPrice);
408
+ const userFilterMaxPriceSignal = signalsService.signal(initialSearchState?.priceRange?.max ?? catalogPriceRange.maxPrice);
409
+ const catalogMinPriceSignal = signalsService.signal(catalogPriceRange.minPrice);
410
+ const catalogMaxPriceSignal = signalsService.signal(catalogPriceRange.maxPrice);
411
+ const availableInventoryStatusesSignal = signalsService.signal([
412
+ InventoryStatusType.IN_STOCK,
413
+ InventoryStatusType.OUT_OF_STOCK,
414
+ InventoryStatusType.PARTIALLY_OUT_OF_STOCK,
415
+ ]);
416
+ const selectedInventoryStatusesSignal = signalsService.signal(initialSearchState?.inventoryStatuses || []);
417
+ const availableProductOptionsSignal = signalsService.signal(getAvailableProductOptions(aggregationData, customizations));
418
+ const selectedProductOptionsSignal = signalsService.signal(initialSearchState?.productOptions || {});
419
+ const selectedCategorySignal = signalsService.signal(initialSearchState?.category || null);
420
+ const selectedVisibleSignal = signalsService.signal(initialSearchState?.visible ?? null);
421
+ const selectedProductTypeSignal = signalsService.signal(initialSearchState?.productType || null);
422
+ const isFilteredSignal = signalsService.signal(false);
423
+ // Computed signals for pagination
424
+ const hasNextPageSignal = signalsService.computed(() => {
425
+ const pagingMetadata = productsListService.pagingMetadata.get();
426
+ return pagingMetadata?.hasNext || false;
427
+ });
428
+ const hasPrevPageSignal = signalsService.computed(() => {
429
+ const pagingMetadata = productsListService.pagingMetadata.get();
430
+ return typeof pagingMetadata.cursors?.prev !== "undefined";
431
+ });
432
+ // Debounce timeout IDs for price filters
433
+ let minPriceTimeoutId = null;
434
+ let maxPriceTimeoutId = null;
435
+ if (typeof window !== "undefined") {
436
+ // Watch for changes in any search parameters and update search options
437
+ signalsService.effect(() => {
438
+ // Read all signals to establish dependencies
439
+ const sort = selectedSortOptionSignal.get();
440
+ const limit = currentLimitSignal.get();
441
+ const cursor = currentCursorSignal.get();
442
+ const minPrice = userFilterMinPriceSignal.get();
443
+ const maxPrice = userFilterMaxPriceSignal.get();
444
+ const selectedInventoryStatuses = selectedInventoryStatusesSignal.get();
445
+ const selectedProductOptions = selectedProductOptionsSignal.get();
446
+ const selectedCategory = selectedCategorySignal.get();
447
+ const selectedVisible = selectedVisibleSignal.get();
448
+ const selectedProductType = selectedProductTypeSignal.get();
449
+ if (firstRun) {
450
+ firstRun = false;
451
+ return;
452
+ }
453
+ // Build complete new search options
454
+ const newSearchOptions = {
455
+ ...productsListService.searchOptions.peek(),
456
+ };
457
+ // Update pagination
458
+ if (limit > 0) {
459
+ newSearchOptions.cursorPaging = {
460
+ limit,
461
+ ...(cursor && { cursor }),
462
+ };
463
+ }
464
+ else {
465
+ delete newSearchOptions.cursorPaging;
466
+ }
467
+ // Update sort
468
+ switch (sort) {
469
+ case SortType.NAME_ASC:
470
+ newSearchOptions.sort = [
471
+ { fieldName: "name", order: productsV3.SortDirection.ASC },
472
+ ];
473
+ break;
474
+ case SortType.NAME_DESC:
475
+ newSearchOptions.sort = [
476
+ { fieldName: "name", order: productsV3.SortDirection.DESC },
477
+ ];
478
+ break;
479
+ case SortType.PRICE_ASC:
480
+ newSearchOptions.sort = [
481
+ {
482
+ fieldName: "actualPriceRange.minValue.amount",
483
+ order: productsV3.SortDirection.ASC,
484
+ },
485
+ ];
486
+ break;
487
+ case SortType.PRICE_DESC:
488
+ newSearchOptions.sort = [
489
+ {
490
+ fieldName: "actualPriceRange.minValue.amount",
491
+ order: productsV3.SortDirection.DESC,
492
+ },
493
+ ];
494
+ break;
495
+ case SortType.RECOMMENDED:
496
+ newSearchOptions.sort = [
497
+ {
498
+ fieldName: "name",
499
+ order: productsV3.SortDirection.DESC,
500
+ },
501
+ ];
502
+ break;
503
+ }
504
+ // Update filters
505
+ if (!newSearchOptions.filter) {
506
+ newSearchOptions.filter = {};
507
+ }
508
+ else {
509
+ newSearchOptions.filter = { ...newSearchOptions.filter };
510
+ }
511
+ // Remove existing filters
512
+ delete newSearchOptions.filter["actualPriceRange.minValue.amount"];
513
+ delete newSearchOptions.filter["actualPriceRange.maxValue.amount"];
514
+ delete newSearchOptions.filter["inventory.availabilityStatus"];
515
+ delete newSearchOptions.filter["allCategoriesInfo.categories"];
516
+ delete newSearchOptions.filter["visible"];
517
+ delete newSearchOptions.filter["productType"];
518
+ // Remove existing product option filters
519
+ Object.keys(newSearchOptions.filter).forEach((key) => {
520
+ if (key.startsWith("options.")) {
521
+ delete newSearchOptions.filter[key];
522
+ }
523
+ });
524
+ // Add new filters
525
+ if (minPrice > 0) {
526
+ newSearchOptions.filter["actualPriceRange.minValue.amount"] = { $gte: minPrice };
527
+ }
528
+ if (maxPrice > 0) {
529
+ newSearchOptions.filter["actualPriceRange.maxValue.amount"] = { $lte: maxPrice };
530
+ }
531
+ if (selectedInventoryStatuses.length > 0) {
532
+ if (selectedInventoryStatuses.length === 1) {
533
+ newSearchOptions.filter["inventory.availabilityStatus"] =
534
+ selectedInventoryStatuses[0];
535
+ }
536
+ else {
537
+ newSearchOptions.filter["inventory.availabilityStatus"] =
538
+ { $in: selectedInventoryStatuses };
539
+ }
540
+ }
541
+ if (selectedProductOptions &&
542
+ Object.keys(selectedProductOptions).length > 0) {
543
+ const allChoiceIds = [];
544
+ for (const choiceIds of Object.values(selectedProductOptions)) {
545
+ allChoiceIds.push(...choiceIds);
546
+ }
547
+ if (allChoiceIds.length > 0) {
548
+ newSearchOptions.filter["options.choicesSettings.choices.choiceId"] = { $hasSome: allChoiceIds };
549
+ }
550
+ }
551
+ if (selectedCategory) {
552
+ newSearchOptions.filter["allCategoriesInfo.categories"] = {
553
+ $matchItems: [{ _id: { $in: [selectedCategory] } }],
554
+ };
555
+ }
556
+ if (selectedVisible !== null) {
557
+ newSearchOptions.filter["visible"] = selectedVisible;
558
+ }
559
+ if (selectedProductType) {
560
+ newSearchOptions.filter["productType"] =
561
+ selectedProductType;
562
+ }
563
+ // Update the products list service
564
+ productsListService.setSearchOptions(newSearchOptions);
565
+ // Update URL with current search state
566
+ const catalogBounds = {
567
+ minPrice: catalogMinPriceSignal.get(),
568
+ maxPrice: catalogMaxPriceSignal.get(),
569
+ };
570
+ const currentFilters = {
571
+ priceRange: { min: minPrice, max: maxPrice },
572
+ inventoryStatuses: selectedInventoryStatuses,
573
+ productOptions: selectedProductOptions,
574
+ ...(selectedCategory && { category: selectedCategory }),
575
+ ...(selectedVisible !== null && { visible: selectedVisible }),
576
+ ...(selectedProductType && { productType: selectedProductType }),
577
+ };
578
+ updateUrlWithSearchState({
579
+ sort,
580
+ filters: currentFilters,
581
+ customizations,
582
+ catalogBounds,
583
+ });
584
+ });
585
+ }
586
+ return {
587
+ // Sort functionality
588
+ selectedSortOption: selectedSortOptionSignal,
589
+ sortOptions: Object.values(SortType),
590
+ setSelectedSortOption: (sort) => selectedSortOptionSignal.set(sort),
591
+ // Pagination functionality
592
+ currentLimit: currentLimitSignal,
593
+ currentCursor: currentCursorSignal,
594
+ hasNextPage: hasNextPageSignal,
595
+ hasPrevPage: hasPrevPageSignal,
596
+ setLimit: (limit) => {
597
+ currentLimitSignal.set(limit);
598
+ currentCursorSignal.set(null); // Reset pagination when changing page size
599
+ },
600
+ loadMore: (count) => {
601
+ const limit = currentLimitSignal.get();
602
+ currentLimitSignal.set(limit + count);
603
+ },
604
+ nextPage: () => {
605
+ const pagingMetadata = productsListService.pagingMetadata.get();
606
+ const nextCursor = pagingMetadata?.cursors?.next;
607
+ if (nextCursor) {
608
+ currentCursorSignal.set(nextCursor);
609
+ }
610
+ },
611
+ prevPage: () => {
612
+ const pagingMetadata = productsListService.pagingMetadata.get();
613
+ const previousCursor = pagingMetadata?.cursors?.prev;
614
+ if (previousCursor) {
615
+ currentCursorSignal.set(previousCursor);
616
+ }
617
+ },
618
+ navigateToFirstPage: () => {
619
+ currentCursorSignal.set(null);
620
+ },
621
+ // Filter functionality
622
+ selectedMinPrice: userFilterMinPriceSignal,
623
+ selectedMaxPrice: userFilterMaxPriceSignal,
624
+ availableMinPrice: catalogMinPriceSignal,
625
+ availableMaxPrice: catalogMaxPriceSignal,
626
+ availableInventoryStatuses: availableInventoryStatusesSignal,
627
+ selectedInventoryStatuses: selectedInventoryStatusesSignal,
628
+ availableProductOptions: availableProductOptionsSignal,
629
+ selectedProductOptions: selectedProductOptionsSignal,
630
+ selectedCategory: selectedCategorySignal,
631
+ setSelectedMinPrice: (minPrice) => {
632
+ if (minPriceTimeoutId) {
633
+ clearTimeout(minPriceTimeoutId);
634
+ }
635
+ minPriceTimeoutId = setTimeout(() => {
636
+ userFilterMinPriceSignal.set(minPrice);
637
+ minPriceTimeoutId = null;
638
+ }, PRICE_FILTER_DEBOUNCE_TIME);
639
+ },
640
+ setSelectedMaxPrice: (maxPrice) => {
641
+ if (maxPriceTimeoutId) {
642
+ clearTimeout(maxPriceTimeoutId);
643
+ }
644
+ maxPriceTimeoutId = setTimeout(() => {
645
+ userFilterMaxPriceSignal.set(maxPrice);
646
+ maxPriceTimeoutId = null;
647
+ }, PRICE_FILTER_DEBOUNCE_TIME);
648
+ },
649
+ toggleInventoryStatus: (status) => {
650
+ const current = selectedInventoryStatusesSignal.get();
651
+ const isSelected = current.includes(status);
652
+ if (isSelected) {
653
+ selectedInventoryStatusesSignal.set(current.filter((s) => s !== status));
654
+ }
655
+ else {
656
+ selectedInventoryStatusesSignal.set([...current, status]);
657
+ }
658
+ },
659
+ toggleProductOption: (optionId, choiceId) => {
660
+ const current = selectedProductOptionsSignal.get();
661
+ const currentChoices = current[optionId] || [];
662
+ const isSelected = currentChoices.includes(choiceId);
663
+ if (isSelected) {
664
+ const newChoices = currentChoices.filter((id) => id !== choiceId);
665
+ if (newChoices.length === 0) {
666
+ const newOptions = { ...current };
667
+ delete newOptions[optionId];
668
+ selectedProductOptionsSignal.set(newOptions);
669
+ }
670
+ else {
671
+ selectedProductOptionsSignal.set({
672
+ ...current,
673
+ [optionId]: newChoices,
674
+ });
675
+ }
676
+ }
677
+ else {
678
+ selectedProductOptionsSignal.set({
679
+ ...current,
680
+ [optionId]: [...currentChoices, choiceId],
681
+ });
682
+ }
683
+ },
684
+ setSelectedCategory: (category) => {
685
+ selectedCategorySignal.set(category);
686
+ },
687
+ setSelectedVisible: (visible) => {
688
+ selectedVisibleSignal.set(visible);
689
+ },
690
+ setSelectedProductType: (productType) => {
691
+ selectedProductTypeSignal.set(productType);
692
+ },
693
+ isFiltered: isFilteredSignal,
694
+ reset: () => {
695
+ selectedSortOptionSignal.set(SortType.NAME_ASC);
696
+ currentLimitSignal.set(DEFAULT_QUERY_LIMIT);
697
+ currentCursorSignal.set(null);
698
+ userFilterMinPriceSignal.set(catalogMinPriceSignal.get());
699
+ userFilterMaxPriceSignal.set(catalogMaxPriceSignal.get());
700
+ selectedInventoryStatusesSignal.set([]);
701
+ selectedProductOptionsSignal.set({});
702
+ selectedCategorySignal.set(null);
703
+ selectedVisibleSignal.set(null);
704
+ selectedProductTypeSignal.set(null);
705
+ isFilteredSignal.set(false);
706
+ },
707
+ };
708
+ });
709
+ // Helper functions (copied from the original services)
710
+ function getCurrentLimit(searchOptions) {
711
+ return searchOptions.cursorPaging?.limit || DEFAULT_QUERY_LIMIT;
712
+ }
713
+ function getCurrentCursor(searchOptions) {
714
+ return searchOptions.cursorPaging?.cursor || null;
715
+ }
716
+ function getCatalogPriceRange(aggregationData) {
717
+ const minPrice = getMinPrice(aggregationData);
718
+ const maxPrice = getMaxPrice(aggregationData);
719
+ return { minPrice, maxPrice };
720
+ }
721
+ function getMinPrice(aggregationData) {
722
+ const minPriceAggregation = aggregationData.find((data) => data.fieldPath === "actualPriceRange.minValue.amount");
723
+ if (minPriceAggregation?.scalar?.value) {
724
+ return Number(minPriceAggregation.scalar.value) || 0;
725
+ }
726
+ return 0;
727
+ }
728
+ function getMaxPrice(aggregationData) {
729
+ const maxPriceAggregation = aggregationData.find((data) => data.fieldPath === "actualPriceRange.maxValue.amount");
730
+ if (maxPriceAggregation?.scalar?.value) {
731
+ return Number(maxPriceAggregation.scalar.value) || 0;
732
+ }
733
+ return 0;
734
+ }
735
+ function getAvailableProductOptions(aggregationData = [], customizations = []) {
736
+ const matchesAggregationName = (name, aggregationNames) => {
737
+ return aggregationNames.some((aggName) => aggName.toLowerCase() === name.toLowerCase());
738
+ };
739
+ const sortChoicesIntelligently = (choices) => {
740
+ return [...choices].sort((a, b) => {
741
+ const aIsNumber = /^\d+$/.test(a.name);
742
+ const bIsNumber = /^\d+$/.test(b.name);
743
+ if (aIsNumber && bIsNumber) {
744
+ return parseInt(a.name) - parseInt(b.name);
745
+ }
746
+ if (aIsNumber && !bIsNumber)
747
+ return -1;
748
+ if (!aIsNumber && bIsNumber)
749
+ return 1;
750
+ return a.name.localeCompare(b.name);
751
+ });
752
+ };
753
+ const optionNames = [];
754
+ const choiceNames = [];
755
+ aggregationData.forEach((result) => {
756
+ if (result.name === "optionNames" && result.values?.results) {
757
+ optionNames.push(...result.values.results
758
+ .map((item) => item.value)
759
+ .filter((value) => typeof value === "string"));
760
+ }
761
+ if (result.name === "choiceNames" && result.values?.results) {
762
+ choiceNames.push(...result.values.results
763
+ .map((item) => item.value)
764
+ .filter((value) => typeof value === "string"));
765
+ }
766
+ });
767
+ const options = customizations
768
+ .filter((customization) => customization.name &&
769
+ customization._id &&
770
+ customization.customizationType ===
771
+ customizationsV3.CustomizationType.PRODUCT_OPTION &&
772
+ (optionNames.length === 0 ||
773
+ matchesAggregationName(customization.name, optionNames)))
774
+ .map((customization) => {
775
+ const choices = (customization.choicesSettings?.choices || [])
776
+ .filter((choice) => choice._id &&
777
+ choice.name &&
778
+ (choiceNames.length === 0 ||
779
+ matchesAggregationName(choice.name, choiceNames)))
780
+ .map((choice) => ({
781
+ id: choice._id,
782
+ name: choice.name,
783
+ colorCode: choice.colorCode,
784
+ }));
785
+ return {
786
+ id: customization._id,
787
+ name: customization.name,
788
+ choices: sortChoicesIntelligently(choices),
789
+ optionRenderType: customization.customizationRenderType,
790
+ };
791
+ })
792
+ .filter((option) => option.choices.length > 0);
793
+ return options;
794
+ }