@shopware/helpers 1.7.2 → 1.8.0

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/README.md CHANGED
@@ -125,8 +125,14 @@ Returns an empty string if `src` is falsy. Returns the original `src` if URL par
125
125
 
126
126
  Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/helpers/CHANGELOG.md)
127
127
 
128
- ### Latest changes: 1.7.2
128
+ ### Latest changes: 1.8.0
129
+
130
+ ### Minor Changes
131
+
132
+ - [#2574](https://github.com/shopware/frontends/pull/2574) [`2ddf156`](https://github.com/shopware/frontends/commit/2ddf156805b2941fe2069e78453fb3c4eb6d44ac) Thanks [@mkucmus](https://github.com/mkucmus)! - Add `getCategoryFilterAggregations()` and `getCategoryFilterPostFilter()` to request category aggregations for product listings and filter by category without reducing the aggregations. `excludeRootCategory()` drops the sales channel entry point from the category entities, and the `CATEGORY_AGGREGATION_NAME` / `CATEGORY_COUNTS_AGGREGATION_NAME` / `CATEGORY_PARENTS_AGGREGATION_NAME` constants are exported for consumers that build the aggregations themselves.
133
+
134
+ `getListingFilters` (`@beta`) merges the `categories` and `categories-counts` response aggregations into a single `categories` filter with a product count per category. This changes the shape of that filter for listings that already requested a `categories` aggregation: the entities are sorted by count (highest first) instead of keeping the response order, each entity gains a `count`, the filter no longer carries the aggregation's `apiAlias`, and `categories-counts` is no longer returned as a filter of its own.
129
135
 
130
136
  ### Patch Changes
131
137
 
132
- - [#2594](https://github.com/shopware/frontends/pull/2594) [`f16c5a0`](https://github.com/shopware/frontends/commit/f16c5a0785d6187b73c3edcf37feab7c90bd7988) Thanks [@patzick](https://github.com/patzick)! - Add helpers for resolving canonical SEO paths and detecting technical Shopware URLs.
138
+ - [#2598](https://github.com/shopware/frontends/pull/2598) [`204c8f4`](https://github.com/shopware/frontends/commit/204c8f45f737e724db6d00b80c5faef8ddb77cb4) Thanks [@dependabot](https://github.com/apps/dependabot)! - Fix Nuxt plugin injection typing for Nuxt 4.5 and maintenance mode error handling.
package/dist/index.d.mts CHANGED
@@ -427,7 +427,7 @@ type HelpersCssClasses = (typeof helpersCssClasses)[number];
427
427
 
428
428
  declare function isMaintenanceMode<T extends {
429
429
  code?: string;
430
- }>(errors: [T]): boolean;
430
+ }>(errors: T[]): boolean;
431
431
 
432
432
  /**
433
433
  * Replace text placeholder with param value
@@ -561,6 +561,61 @@ declare function getCanonicalPathForTechnicalPath(path: string, seoUrl?: object
561
561
  */
562
562
  declare function getTranslatedProperty<T>(element: T | undefined | null | never, property: keyof T): string;
563
563
 
564
+ declare const CATEGORY_AGGREGATION_NAME = "categories";
565
+ declare const CATEGORY_COUNTS_AGGREGATION_NAME = "categories-counts";
566
+ declare const CATEGORY_PARENTS_AGGREGATION_NAME = "categories-parents";
567
+ type CategoryEntityAggregation = {
568
+ name: string;
569
+ type: "entity";
570
+ definition: "category";
571
+ field: string;
572
+ };
573
+ type CategoryCountsAggregation = {
574
+ name: string;
575
+ type: "terms";
576
+ field: string;
577
+ };
578
+ type CategoryFilterBucket = {
579
+ key: string;
580
+ count: number;
581
+ } & {
582
+ [nestedAggregationName: string]: unknown;
583
+ };
584
+ /**
585
+ * Category aggregations for a listing filter: an entity `categories` agg plus a
586
+ * flat `categories-counts` terms agg. Kept flat because the ES search route
587
+ * returns no buckets once a nested sub-agg is attached, so variants overcount.
588
+ *
589
+ * @beta
590
+ */
591
+ declare function getCategoryFilterAggregations(): Array<CategoryEntityAggregation | CategoryCountsAggregation>;
592
+ /**
593
+ * Category filter entities without the sales channel entry point.
594
+ *
595
+ * That root category is an ancestor of every product's category tree, so it
596
+ * matches the whole result set and filtering by it changes nothing. Opt in per
597
+ * listing by passing `sessionContext.salesChannel.navigationCategoryId`;
598
+ * without an id the entities are returned unchanged.
599
+ *
600
+ * @beta
601
+ */
602
+ declare function excludeRootCategory<T extends {
603
+ id: string;
604
+ }>(entities: T[] | undefined | null, rootCategoryId: string | undefined | null): T[];
605
+ /**
606
+ * Criteria `post-filter` entry narrowing a product listing to the given
607
+ * category ids. Sent as a post-filter, it does not reduce the category
608
+ * aggregations from `getCategoryFilterAggregations`, so all category options
609
+ * stay visible while results are filtered (standard faceted behavior).
610
+ *
611
+ * @beta
612
+ */
613
+ declare function getCategoryFilterPostFilter(categoryIds: string[]): {
614
+ field: string;
615
+ type: "equalsAny";
616
+ value: string;
617
+ };
618
+
564
619
  type ListingFilter = {
565
620
  label: string;
566
621
  code: string;
@@ -577,6 +632,7 @@ type ListingFilter = {
577
632
  translated?: {
578
633
  name?: string;
579
634
  };
635
+ count?: number;
580
636
  }>;
581
637
  };
582
638
  /**
@@ -781,5 +837,5 @@ declare function canUseQuoteActions<T extends {
781
837
  };
782
838
  }>(quote: T): boolean;
783
839
 
784
- export { buildCdnImageUrl, buildUrlPrefix, canUseQuoteActions, downloadFile, encodeUrlPath, generateCdnSrcSet, getBackgroundImageUrl, getBiggestThumbnailUrl, getCanonicalPathForTechnicalPath, getCategoryBreadcrumbs, getCategoryImageUrl, getCategoryRoute, getCategoryUrl, getCmsBreadcrumbs, getCmsEntityObject, getCmsLayoutConfiguration, getCmsTranslate, getFormattedPrice, getLanguageName, getListingFilters, getMainImageUrl, getMedia, getPaymentMethodIcon, getProductCalculatedListingPrice, getProductFreeShipping, getProductFromPrice, getProductListingFromCmsPage, getProductManufacturerName, getProductName, getProductRatingAverage, getProductReviews, getProductRoute, getProductTierPrices, getProductUrl, getRouteFromPathInfo, getShippingMethodDeliveryTime, getShippingMethodIcon, getSmallestThumbnailUrl, getSrcSetForMedia, getTranslatedProperty, helpersCssClasses, isCategory, isLandingPage, isMaintenanceMode, isProduct, isProductOnSale, isProductTopSeller, isTechnicalPath, isTechnicalUrl, normalizePath, relativeUrlSlash, urlIsAbsolute };
785
- export type { BackgroundImageOptions, HelpersCssClasses, LayoutConfiguration, RouteInfoFromPathInfo, RouteNameFromPathInfo, TierPrice, UiMediaGalleryItem, UiMediaGalleryItemUrl, UiProductOption, UiProductProperty, UiProductReview, UrlRouteOutput };
840
+ export { CATEGORY_AGGREGATION_NAME, CATEGORY_COUNTS_AGGREGATION_NAME, CATEGORY_PARENTS_AGGREGATION_NAME, buildCdnImageUrl, buildUrlPrefix, canUseQuoteActions, downloadFile, encodeUrlPath, excludeRootCategory, generateCdnSrcSet, getBackgroundImageUrl, getBiggestThumbnailUrl, getCanonicalPathForTechnicalPath, getCategoryBreadcrumbs, getCategoryFilterAggregations, getCategoryFilterPostFilter, getCategoryImageUrl, getCategoryRoute, getCategoryUrl, getCmsBreadcrumbs, getCmsEntityObject, getCmsLayoutConfiguration, getCmsTranslate, getFormattedPrice, getLanguageName, getListingFilters, getMainImageUrl, getMedia, getPaymentMethodIcon, getProductCalculatedListingPrice, getProductFreeShipping, getProductFromPrice, getProductListingFromCmsPage, getProductManufacturerName, getProductName, getProductRatingAverage, getProductReviews, getProductRoute, getProductTierPrices, getProductUrl, getRouteFromPathInfo, getShippingMethodDeliveryTime, getShippingMethodIcon, getSmallestThumbnailUrl, getSrcSetForMedia, getTranslatedProperty, helpersCssClasses, isCategory, isLandingPage, isMaintenanceMode, isProduct, isProductOnSale, isProductTopSeller, isTechnicalPath, isTechnicalUrl, normalizePath, relativeUrlSlash, urlIsAbsolute };
841
+ export type { BackgroundImageOptions, CategoryFilterBucket, HelpersCssClasses, LayoutConfiguration, RouteInfoFromPathInfo, RouteNameFromPathInfo, TierPrice, UiMediaGalleryItem, UiMediaGalleryItemUrl, UiProductOption, UiProductProperty, UiProductReview, UrlRouteOutput };
package/dist/index.d.ts CHANGED
@@ -427,7 +427,7 @@ type HelpersCssClasses = (typeof helpersCssClasses)[number];
427
427
 
428
428
  declare function isMaintenanceMode<T extends {
429
429
  code?: string;
430
- }>(errors: [T]): boolean;
430
+ }>(errors: T[]): boolean;
431
431
 
432
432
  /**
433
433
  * Replace text placeholder with param value
@@ -561,6 +561,61 @@ declare function getCanonicalPathForTechnicalPath(path: string, seoUrl?: object
561
561
  */
562
562
  declare function getTranslatedProperty<T>(element: T | undefined | null | never, property: keyof T): string;
563
563
 
564
+ declare const CATEGORY_AGGREGATION_NAME = "categories";
565
+ declare const CATEGORY_COUNTS_AGGREGATION_NAME = "categories-counts";
566
+ declare const CATEGORY_PARENTS_AGGREGATION_NAME = "categories-parents";
567
+ type CategoryEntityAggregation = {
568
+ name: string;
569
+ type: "entity";
570
+ definition: "category";
571
+ field: string;
572
+ };
573
+ type CategoryCountsAggregation = {
574
+ name: string;
575
+ type: "terms";
576
+ field: string;
577
+ };
578
+ type CategoryFilterBucket = {
579
+ key: string;
580
+ count: number;
581
+ } & {
582
+ [nestedAggregationName: string]: unknown;
583
+ };
584
+ /**
585
+ * Category aggregations for a listing filter: an entity `categories` agg plus a
586
+ * flat `categories-counts` terms agg. Kept flat because the ES search route
587
+ * returns no buckets once a nested sub-agg is attached, so variants overcount.
588
+ *
589
+ * @beta
590
+ */
591
+ declare function getCategoryFilterAggregations(): Array<CategoryEntityAggregation | CategoryCountsAggregation>;
592
+ /**
593
+ * Category filter entities without the sales channel entry point.
594
+ *
595
+ * That root category is an ancestor of every product's category tree, so it
596
+ * matches the whole result set and filtering by it changes nothing. Opt in per
597
+ * listing by passing `sessionContext.salesChannel.navigationCategoryId`;
598
+ * without an id the entities are returned unchanged.
599
+ *
600
+ * @beta
601
+ */
602
+ declare function excludeRootCategory<T extends {
603
+ id: string;
604
+ }>(entities: T[] | undefined | null, rootCategoryId: string | undefined | null): T[];
605
+ /**
606
+ * Criteria `post-filter` entry narrowing a product listing to the given
607
+ * category ids. Sent as a post-filter, it does not reduce the category
608
+ * aggregations from `getCategoryFilterAggregations`, so all category options
609
+ * stay visible while results are filtered (standard faceted behavior).
610
+ *
611
+ * @beta
612
+ */
613
+ declare function getCategoryFilterPostFilter(categoryIds: string[]): {
614
+ field: string;
615
+ type: "equalsAny";
616
+ value: string;
617
+ };
618
+
564
619
  type ListingFilter = {
565
620
  label: string;
566
621
  code: string;
@@ -577,6 +632,7 @@ type ListingFilter = {
577
632
  translated?: {
578
633
  name?: string;
579
634
  };
635
+ count?: number;
580
636
  }>;
581
637
  };
582
638
  /**
@@ -781,5 +837,5 @@ declare function canUseQuoteActions<T extends {
781
837
  };
782
838
  }>(quote: T): boolean;
783
839
 
784
- export { buildCdnImageUrl, buildUrlPrefix, canUseQuoteActions, downloadFile, encodeUrlPath, generateCdnSrcSet, getBackgroundImageUrl, getBiggestThumbnailUrl, getCanonicalPathForTechnicalPath, getCategoryBreadcrumbs, getCategoryImageUrl, getCategoryRoute, getCategoryUrl, getCmsBreadcrumbs, getCmsEntityObject, getCmsLayoutConfiguration, getCmsTranslate, getFormattedPrice, getLanguageName, getListingFilters, getMainImageUrl, getMedia, getPaymentMethodIcon, getProductCalculatedListingPrice, getProductFreeShipping, getProductFromPrice, getProductListingFromCmsPage, getProductManufacturerName, getProductName, getProductRatingAverage, getProductReviews, getProductRoute, getProductTierPrices, getProductUrl, getRouteFromPathInfo, getShippingMethodDeliveryTime, getShippingMethodIcon, getSmallestThumbnailUrl, getSrcSetForMedia, getTranslatedProperty, helpersCssClasses, isCategory, isLandingPage, isMaintenanceMode, isProduct, isProductOnSale, isProductTopSeller, isTechnicalPath, isTechnicalUrl, normalizePath, relativeUrlSlash, urlIsAbsolute };
785
- export type { BackgroundImageOptions, HelpersCssClasses, LayoutConfiguration, RouteInfoFromPathInfo, RouteNameFromPathInfo, TierPrice, UiMediaGalleryItem, UiMediaGalleryItemUrl, UiProductOption, UiProductProperty, UiProductReview, UrlRouteOutput };
840
+ export { CATEGORY_AGGREGATION_NAME, CATEGORY_COUNTS_AGGREGATION_NAME, CATEGORY_PARENTS_AGGREGATION_NAME, buildCdnImageUrl, buildUrlPrefix, canUseQuoteActions, downloadFile, encodeUrlPath, excludeRootCategory, generateCdnSrcSet, getBackgroundImageUrl, getBiggestThumbnailUrl, getCanonicalPathForTechnicalPath, getCategoryBreadcrumbs, getCategoryFilterAggregations, getCategoryFilterPostFilter, getCategoryImageUrl, getCategoryRoute, getCategoryUrl, getCmsBreadcrumbs, getCmsEntityObject, getCmsLayoutConfiguration, getCmsTranslate, getFormattedPrice, getLanguageName, getListingFilters, getMainImageUrl, getMedia, getPaymentMethodIcon, getProductCalculatedListingPrice, getProductFreeShipping, getProductFromPrice, getProductListingFromCmsPage, getProductManufacturerName, getProductName, getProductRatingAverage, getProductReviews, getProductRoute, getProductTierPrices, getProductUrl, getRouteFromPathInfo, getShippingMethodDeliveryTime, getShippingMethodIcon, getSmallestThumbnailUrl, getSrcSetForMedia, getTranslatedProperty, helpersCssClasses, isCategory, isLandingPage, isMaintenanceMode, isProduct, isProductOnSale, isProductTopSeller, isTechnicalPath, isTechnicalUrl, normalizePath, relativeUrlSlash, urlIsAbsolute };
841
+ export type { BackgroundImageOptions, CategoryFilterBucket, HelpersCssClasses, LayoutConfiguration, RouteInfoFromPathInfo, RouteNameFromPathInfo, TierPrice, UiMediaGalleryItem, UiMediaGalleryItemUrl, UiProductOption, UiProductProperty, UiProductReview, UrlRouteOutput };
package/dist/index.mjs CHANGED
@@ -396,6 +396,48 @@ function getCanonicalPathForTechnicalPath(path, seoUrl) {
396
396
  return canonicalPath;
397
397
  }
398
398
 
399
+ const CATEGORY_AGGREGATION_NAME = "categories";
400
+ const CATEGORY_COUNTS_AGGREGATION_NAME = "categories-counts";
401
+ const CATEGORY_PARENTS_AGGREGATION_NAME = "categories-parents";
402
+ const CATEGORY_FIELD = "categoriesRo.id";
403
+ function getCategoryFilterAggregations() {
404
+ return [
405
+ {
406
+ name: CATEGORY_AGGREGATION_NAME,
407
+ type: "entity",
408
+ definition: "category",
409
+ field: CATEGORY_FIELD
410
+ },
411
+ {
412
+ name: CATEGORY_COUNTS_AGGREGATION_NAME,
413
+ type: "terms",
414
+ field: CATEGORY_FIELD
415
+ }
416
+ ];
417
+ }
418
+ function excludeRootCategory(entities, rootCategoryId) {
419
+ if (!entities) return [];
420
+ return entities.filter((entity) => entity.id !== rootCategoryId);
421
+ }
422
+ function getCategoryFilterPostFilter(categoryIds) {
423
+ return {
424
+ field: CATEGORY_FIELD,
425
+ type: "equalsAny",
426
+ value: categoryIds.join("|")
427
+ };
428
+ }
429
+ function resolveCategoryBucketCount(bucket) {
430
+ const parents = bucket[CATEGORY_PARENTS_AGGREGATION_NAME];
431
+ if (!parents?.buckets) {
432
+ return bucket.count;
433
+ }
434
+ let count = 0;
435
+ for (const parent of parents.buckets) {
436
+ count += parent.key ? 1 : parent.count;
437
+ }
438
+ return count;
439
+ }
440
+
399
441
  const getFilter = (code, aggregation) => {
400
442
  return {
401
443
  label: getTranslatedProperty(aggregation, "name") || code,
@@ -406,6 +448,30 @@ const getFilter = (code, aggregation) => {
406
448
  function isEntitiesAggregation(aggregation) {
407
449
  return aggregation.entities !== void 0;
408
450
  }
451
+ function getCategoryFilter(aggregation, countsAggregation) {
452
+ const countByCategoryId = /* @__PURE__ */ new Map();
453
+ for (const bucket of countsAggregation?.buckets ?? []) {
454
+ countByCategoryId.set(bucket.key, resolveCategoryBucketCount(bucket));
455
+ }
456
+ const entities = aggregation.entities.map((entity) => ({
457
+ ...entity,
458
+ // Count per category. Used to sort here and exposed to consumers, but on
459
+ // the ES search route it counts every variant, so the filter UIs shipped
460
+ // in this repo do not render it.
461
+ count: countByCategoryId.get(entity.id)
462
+ })).sort(
463
+ (a, b) => (b.count ?? 0) - (a.count ?? 0) || getTranslatedProperty(a, "name").localeCompare(
464
+ getTranslatedProperty(b, "name")
465
+ )
466
+ );
467
+ return {
468
+ label: CATEGORY_AGGREGATION_NAME,
469
+ code: CATEGORY_AGGREGATION_NAME,
470
+ id: CATEGORY_AGGREGATION_NAME,
471
+ name: CATEGORY_AGGREGATION_NAME,
472
+ entities
473
+ };
474
+ }
409
475
  function getListingFilters(aggregations) {
410
476
  if (!aggregations) {
411
477
  return [];
@@ -416,7 +482,16 @@ function getListingFilters(aggregations) {
416
482
  for (const filterEntity of aggregation.entities) {
417
483
  transformedFilters.push(getFilter(aggregationName, filterEntity));
418
484
  }
419
- } else if (!["properties", "options"].includes(aggregationName)) {
485
+ } else if (aggregationName === CATEGORY_AGGREGATION_NAME && isEntitiesAggregation(aggregation)) {
486
+ transformedFilters.push(
487
+ getCategoryFilter(
488
+ aggregation,
489
+ aggregations[CATEGORY_COUNTS_AGGREGATION_NAME]
490
+ )
491
+ );
492
+ } else if (!["properties", "options", CATEGORY_COUNTS_AGGREGATION_NAME].includes(
493
+ aggregationName
494
+ )) {
420
495
  transformedFilters.push(getFilter(aggregationName, aggregation));
421
496
  }
422
497
  }
@@ -580,4 +655,4 @@ function canUseQuoteActions(quote) {
580
655
  return ["replied"].includes(quote.stateMachineState?.technicalName ?? "");
581
656
  }
582
657
 
583
- export { buildCdnImageUrl, buildUrlPrefix, canUseQuoteActions, downloadFile, encodeUrlPath, generateCdnSrcSet, getBackgroundImageUrl, getBiggestThumbnailUrl, getCanonicalPathForTechnicalPath, getCategoryBreadcrumbs, getCategoryImageUrl, getCategoryRoute, getCategoryUrl, getCmsBreadcrumbs, getCmsEntityObject, getCmsLayoutConfiguration, getCmsTranslate, getFormattedPrice, getLanguageName, getListingFilters, getMainImageUrl, getMedia, getPaymentMethodIcon, getProductCalculatedListingPrice, getProductFreeShipping, getProductFromPrice, getProductListingFromCmsPage, getProductManufacturerName, getProductName, getProductRatingAverage, getProductReviews, getProductRoute, getProductTierPrices, getProductUrl, getRouteFromPathInfo, getShippingMethodDeliveryTime, getShippingMethodIcon, getSmallestThumbnailUrl, getSrcSetForMedia, getTranslatedProperty, helpersCssClasses, isCategory, isLandingPage, isMaintenanceMode, isProduct, isProductOnSale, isProductTopSeller, isTechnicalPath, isTechnicalUrl, normalizePath, relativeUrlSlash, urlIsAbsolute };
658
+ export { CATEGORY_AGGREGATION_NAME, CATEGORY_COUNTS_AGGREGATION_NAME, CATEGORY_PARENTS_AGGREGATION_NAME, buildCdnImageUrl, buildUrlPrefix, canUseQuoteActions, downloadFile, encodeUrlPath, excludeRootCategory, generateCdnSrcSet, getBackgroundImageUrl, getBiggestThumbnailUrl, getCanonicalPathForTechnicalPath, getCategoryBreadcrumbs, getCategoryFilterAggregations, getCategoryFilterPostFilter, getCategoryImageUrl, getCategoryRoute, getCategoryUrl, getCmsBreadcrumbs, getCmsEntityObject, getCmsLayoutConfiguration, getCmsTranslate, getFormattedPrice, getLanguageName, getListingFilters, getMainImageUrl, getMedia, getPaymentMethodIcon, getProductCalculatedListingPrice, getProductFreeShipping, getProductFromPrice, getProductListingFromCmsPage, getProductManufacturerName, getProductName, getProductRatingAverage, getProductReviews, getProductRoute, getProductTierPrices, getProductUrl, getRouteFromPathInfo, getShippingMethodDeliveryTime, getShippingMethodIcon, getSmallestThumbnailUrl, getSrcSetForMedia, getTranslatedProperty, helpersCssClasses, isCategory, isLandingPage, isMaintenanceMode, isProduct, isProductOnSale, isProductTopSeller, isTechnicalPath, isTechnicalUrl, normalizePath, relativeUrlSlash, urlIsAbsolute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopware/helpers",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Shopware helpers for accessing API data",
5
5
  "author": "Shopware",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "devDependencies": {
35
35
  "@typescript/native-preview": "7.0.0-dev.20260205.1",
36
36
  "@vitest/coverage-v8": "4.1.10",
37
- "happy-dom": "20.10.6",
37
+ "happy-dom": "20.11.1",
38
38
  "unbuild": "3.6.1",
39
39
  "vitest": "4.1.10",
40
40
  "tsconfig": "0.0.0"