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