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