@propeller-commerce/propeller-v2-react-ui 0.12.0 → 0.13.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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _propeller_commerce_propeller_v2_core_ui from '@propeller-commerce/propeller-v2-core-ui';
2
- import { Services, ShopMode, UserMode, PriceComponentProps, StockComponentProps, AddToCartComponentProps, ImageComponentProps, BadgesComponentProps, FavoriteComponentProps, ProductBundlesComponentProps, ProductBulkPricesComponentProps, ProductSurchargesComponentProps, AnyUser, Result } from '@propeller-commerce/propeller-v2-core-ui';
2
+ import { Services, ShopMode, UserMode, PriceComponentProps, StockComponentProps, AddToCartComponentProps, ImageComponentProps, BadgesComponentProps, FavoriteComponentProps, ProductBundlesComponentProps, ProductBulkPricesComponentProps, ProductSurchargesComponentProps, AnyUser, Result, Availability } from '@propeller-commerce/propeller-v2-core-ui';
3
3
  export * from '@propeller-commerce/propeller-v2-core-ui';
4
4
  import * as React$1 from 'react';
5
5
  import React__default, { ReactNode, ComponentType } from 'react';
@@ -1395,6 +1395,12 @@ interface UseProductSearchOptions {
1395
1395
  priceFilterMin?: number;
1396
1396
  /** Upper price bound filter. */
1397
1397
  priceFilterMax?: number;
1398
+ /**
1399
+ * Stock buckets to filter by. Empty, unset, or both buckets sends no stock
1400
+ * filter. Filtering happens server-side, so `itemsFound` and the page count
1401
+ * describe the filtered set.
1402
+ */
1403
+ availability?: Availability[];
1398
1404
  /** Sort field; overrides the internal sort state when set. */
1399
1405
  sortField?: string;
1400
1406
  /** Sort direction (`'ASC'` / `'DESC'`); overrides internal state when set. */
@@ -4618,6 +4624,23 @@ interface GridFiltersProps {
4618
4624
  activePriceMin?: number;
4619
4625
  /** Currently active price filter max from URL state. */
4620
4626
  activePriceMax?: number;
4627
+ /**
4628
+ * Show the availability (stock) section. Defaults to false so hosts opt in
4629
+ * rather than gaining a new filter section on upgrade.
4630
+ *
4631
+ * The host should pass its own "show stock" setting here — a stock filter
4632
+ * is meaningless when no stock is displayed anywhere in the grid. When
4633
+ * truthy the section is still subject to the same semi-closed-portal
4634
+ * visibility rule as the price filter (hidden for anonymous users).
4635
+ */
4636
+ showAvailabilityFilter?: boolean;
4637
+ /**
4638
+ * Currently active availability buckets (URL-driven). Seeds the checkboxes
4639
+ * on the first render so a restored filtered URL renders them ticked.
4640
+ */
4641
+ activeAvailability?: Availability[];
4642
+ /** Called whenever an availability checkbox is toggled. */
4643
+ onAvailabilityChange?: (sel: Availability[]) => void;
4621
4644
  /**
4622
4645
  * When true, all checkboxes and price inputs are disabled.
4623
4646
  * Wire to ProductGrid's `onLoadingChange` to block rapid re-clicks while a fetch is in flight.
@@ -4802,6 +4825,16 @@ interface GridToolbarProps {
4802
4825
  * Called when the price filter badge × is clicked.
4803
4826
  */
4804
4827
  onPriceFilterRemove?: () => void;
4828
+ /**
4829
+ * Currently active availability (stock) filter selections.
4830
+ * Renders one removable chip per selected bucket.
4831
+ */
4832
+ availability?: Availability[];
4833
+ /**
4834
+ * Called when an availability filter badge × is clicked.
4835
+ * Receives the specific bucket to remove.
4836
+ */
4837
+ onAvailabilityFilterRemove?: (value: Availability) => void;
4805
4838
  /**
4806
4839
  * Called when "Clear All" is clicked.
4807
4840
  */
@@ -6195,6 +6228,11 @@ interface ProductGridProps {
6195
6228
  /** * Active text filters to apply — built by the parent from FiltersSidebar * `onFilterChange` callbacks. Each entry maps to a `textFilters` input * row in the CategoryService query. * When this prop changes the grid automatically re-fetches (page resets to 1). */ textFilters?: ProductTextFilterInput[];
6196
6229
  /** * Active price range lower bound from the FiltersSidebar `onPriceChange`. * Triggers a re-fetch when changed. */ priceFilterMin?: number;
6197
6230
  /** * Active price range upper bound from the FiltersSidebar `onPriceChange`. * Triggers a re-fetch when changed. */ priceFilterMax?: number;
6231
+ /**
6232
+ * Active stock buckets from the filters sidebar. Triggers a re-fetch when
6233
+ * changed. Empty or both buckets means no stock filter.
6234
+ */
6235
+ availability?: Availability[];
6198
6236
  /** * Called when sort state changes internally (for syncing a sibling toolbar). */ onSortChange?: (field: string, order: string) => void;
6199
6237
  /** * Called after each internal data fetch with the min/max price of the * current product set — use to populate a price range slider in the parent. */ onPriceBoundsChange?: (min: number, max: number) => void;
6200
6238
  /** * Called after each fetch with the total number of products found — * use to display a result count in the parent toolbar. */ onItemsFoundChange?: (count: number) => void;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _propeller_commerce_propeller_v2_core_ui from '@propeller-commerce/propeller-v2-core-ui';
2
- import { Services, ShopMode, UserMode, PriceComponentProps, StockComponentProps, AddToCartComponentProps, ImageComponentProps, BadgesComponentProps, FavoriteComponentProps, ProductBundlesComponentProps, ProductBulkPricesComponentProps, ProductSurchargesComponentProps, AnyUser, Result } from '@propeller-commerce/propeller-v2-core-ui';
2
+ import { Services, ShopMode, UserMode, PriceComponentProps, StockComponentProps, AddToCartComponentProps, ImageComponentProps, BadgesComponentProps, FavoriteComponentProps, ProductBundlesComponentProps, ProductBulkPricesComponentProps, ProductSurchargesComponentProps, AnyUser, Result, Availability } from '@propeller-commerce/propeller-v2-core-ui';
3
3
  export * from '@propeller-commerce/propeller-v2-core-ui';
4
4
  import * as React$1 from 'react';
5
5
  import React__default, { ReactNode, ComponentType } from 'react';
@@ -1395,6 +1395,12 @@ interface UseProductSearchOptions {
1395
1395
  priceFilterMin?: number;
1396
1396
  /** Upper price bound filter. */
1397
1397
  priceFilterMax?: number;
1398
+ /**
1399
+ * Stock buckets to filter by. Empty, unset, or both buckets sends no stock
1400
+ * filter. Filtering happens server-side, so `itemsFound` and the page count
1401
+ * describe the filtered set.
1402
+ */
1403
+ availability?: Availability[];
1398
1404
  /** Sort field; overrides the internal sort state when set. */
1399
1405
  sortField?: string;
1400
1406
  /** Sort direction (`'ASC'` / `'DESC'`); overrides internal state when set. */
@@ -4618,6 +4624,23 @@ interface GridFiltersProps {
4618
4624
  activePriceMin?: number;
4619
4625
  /** Currently active price filter max from URL state. */
4620
4626
  activePriceMax?: number;
4627
+ /**
4628
+ * Show the availability (stock) section. Defaults to false so hosts opt in
4629
+ * rather than gaining a new filter section on upgrade.
4630
+ *
4631
+ * The host should pass its own "show stock" setting here — a stock filter
4632
+ * is meaningless when no stock is displayed anywhere in the grid. When
4633
+ * truthy the section is still subject to the same semi-closed-portal
4634
+ * visibility rule as the price filter (hidden for anonymous users).
4635
+ */
4636
+ showAvailabilityFilter?: boolean;
4637
+ /**
4638
+ * Currently active availability buckets (URL-driven). Seeds the checkboxes
4639
+ * on the first render so a restored filtered URL renders them ticked.
4640
+ */
4641
+ activeAvailability?: Availability[];
4642
+ /** Called whenever an availability checkbox is toggled. */
4643
+ onAvailabilityChange?: (sel: Availability[]) => void;
4621
4644
  /**
4622
4645
  * When true, all checkboxes and price inputs are disabled.
4623
4646
  * Wire to ProductGrid's `onLoadingChange` to block rapid re-clicks while a fetch is in flight.
@@ -4802,6 +4825,16 @@ interface GridToolbarProps {
4802
4825
  * Called when the price filter badge × is clicked.
4803
4826
  */
4804
4827
  onPriceFilterRemove?: () => void;
4828
+ /**
4829
+ * Currently active availability (stock) filter selections.
4830
+ * Renders one removable chip per selected bucket.
4831
+ */
4832
+ availability?: Availability[];
4833
+ /**
4834
+ * Called when an availability filter badge × is clicked.
4835
+ * Receives the specific bucket to remove.
4836
+ */
4837
+ onAvailabilityFilterRemove?: (value: Availability) => void;
4805
4838
  /**
4806
4839
  * Called when "Clear All" is clicked.
4807
4840
  */
@@ -6195,6 +6228,11 @@ interface ProductGridProps {
6195
6228
  /** * Active text filters to apply — built by the parent from FiltersSidebar * `onFilterChange` callbacks. Each entry maps to a `textFilters` input * row in the CategoryService query. * When this prop changes the grid automatically re-fetches (page resets to 1). */ textFilters?: ProductTextFilterInput[];
6196
6229
  /** * Active price range lower bound from the FiltersSidebar `onPriceChange`. * Triggers a re-fetch when changed. */ priceFilterMin?: number;
6197
6230
  /** * Active price range upper bound from the FiltersSidebar `onPriceChange`. * Triggers a re-fetch when changed. */ priceFilterMax?: number;
6231
+ /**
6232
+ * Active stock buckets from the filters sidebar. Triggers a re-fetch when
6233
+ * changed. Empty or both buckets means no stock filter.
6234
+ */
6235
+ availability?: Availability[];
6198
6236
  /** * Called when sort state changes internally (for syncing a sibling toolbar). */ onSortChange?: (field: string, order: string) => void;
6199
6237
  /** * Called after each internal data fetch with the min/max price of the * current product set — use to populate a price range slider in the parent. */ onPriceBoundsChange?: (min: number, max: number) => void;
6200
6238
  /** * Called after each fetch with the total number of products found — * use to display a result count in the parent toolbar. */ onItemsFoundChange?: (count: number) => void;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { getLabel, getLocalizedValue, formatPrice, getClusterSku, getClusterImageUrl, isContentHidden, getProductSku, getProductImageUrl, createServices, err, ok, formatSurcharge, deriveUserMode, isContact, isCustomer, getAddresses, attributeNameMatches, getAttributeDisplayName, filterProductsBySelections, extractAttributeValues, getLanguageString, collectAttributeValues, getCountryName } from '@propeller-commerce/propeller-v2-core-ui';
2
+ import { getLabel, getLocalizedValue, formatPrice, getClusterSku, getClusterImageUrl, isContentHidden, getProductSku, getProductImageUrl, createServices, err, ok, formatSurcharge, deriveUserMode, isContact, isCustomer, getAddresses, attributeNameMatches, getAttributeDisplayName, filterProductsBySelections, extractAttributeValues, buildInventoryFilter, getLanguageString, collectAttributeValues, getCountryName } from '@propeller-commerce/propeller-v2-core-ui';
3
3
  export * from '@propeller-commerce/propeller-v2-core-ui';
4
4
  import * as React6 from 'react';
5
5
  import { createContext, useState, useRef, useMemo, useCallback, useContext, useEffect } from 'react';
@@ -2241,7 +2241,8 @@ function useProductSearch(options) {
2241
2241
  userKey,
2242
2242
  JSON.stringify(options.textFilters ?? []),
2243
2243
  options.priceFilterMin ?? "",
2244
- options.priceFilterMax ?? ""
2244
+ options.priceFilterMax ?? "",
2245
+ JSON.stringify(options.availability ?? [])
2245
2246
  ].join("|");
2246
2247
  if (inflightFetches2.has(inflightKey)) {
2247
2248
  fetchIdRef.current--;
@@ -2283,6 +2284,7 @@ function useProductSearch(options) {
2283
2284
  }
2284
2285
  ] : [];
2285
2286
  const priceFilter = options.priceFilterMin !== void 0 || options.priceFilterMax !== void 0 ? { from: options.priceFilterMin ?? 0, to: options.priceFilterMax ?? 999999 } : void 0;
2287
+ const inventoryFilter = buildInventoryFilter(options.availability);
2286
2288
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2287
2289
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2288
2290
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
@@ -2305,6 +2307,7 @@ function useProductSearch(options) {
2305
2307
  ...options.brand && { manufacturers: [options.brand] },
2306
2308
  ...options.textFilters?.length && { textFilters: options.textFilters },
2307
2309
  ...priceFilter && { price: priceFilter },
2310
+ ...inventoryFilter && { inventory: inventoryFilter },
2308
2311
  ...sortInputs.length && { sortInputs },
2309
2312
  ...options.companyId && { companyId: options.companyId },
2310
2313
  ...userId !== void 0 && { userId },
@@ -2381,6 +2384,7 @@ function useProductSearch(options) {
2381
2384
  options.textFilters,
2382
2385
  options.priceFilterMin,
2383
2386
  options.priceFilterMax,
2387
+ options.availability,
2384
2388
  options.sortField,
2385
2389
  options.sortOrder,
2386
2390
  options.companyId,
@@ -2420,6 +2424,7 @@ function useProductSearch(options) {
2420
2424
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2421
2425
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2422
2426
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
2427
+ const inventoryFilter = buildInventoryFilter(options.availability);
2423
2428
  const categoryProductSearchInput = {
2424
2429
  language,
2425
2430
  page: 1,
@@ -2431,6 +2436,7 @@ function useProductSearch(options) {
2431
2436
  ProductStatus.S
2432
2437
  ],
2433
2438
  hidden: false,
2439
+ ...inventoryFilter && { inventory: inventoryFilter },
2434
2440
  term,
2435
2441
  searchFields: [
2436
2442
  {
@@ -2510,6 +2516,7 @@ function useProductSearch(options) {
2510
2516
  JSON.stringify(options.textFilters),
2511
2517
  options.priceFilterMin,
2512
2518
  options.priceFilterMax,
2519
+ options.availability,
2513
2520
  options.sortField,
2514
2521
  options.sortOrder,
2515
2522
  pageSize,
@@ -9032,6 +9039,7 @@ function ProductGrid(rawProps) {
9032
9039
  textFilters: props.textFilters,
9033
9040
  priceFilterMin: props.priceFilterMin,
9034
9041
  priceFilterMax: props.priceFilterMax,
9042
+ availability: props.availability,
9035
9043
  sortField: props.sortField,
9036
9044
  sortOrder: props.sortOrder,
9037
9045
  pageSize: props.pageSize,
@@ -9334,11 +9342,18 @@ function GridFilters(rawProps) {
9334
9342
  const [isPending, setIsPending] = useState(() => false);
9335
9343
  const [appliedMin, setAppliedMin] = useState(() => void 0);
9336
9344
  const [appliedMax, setAppliedMax] = useState(() => void 0);
9345
+ const [availability, setAvailability] = useState(
9346
+ () => [...props.activeAvailability || []]
9347
+ );
9337
9348
  function showPriceFilter() {
9338
9349
  const mode = props.portalMode || "open";
9339
9350
  if (mode === "open") return true;
9340
9351
  return !!props.user;
9341
9352
  }
9353
+ function showAvailability() {
9354
+ if (!props.showAvailabilityFilter) return false;
9355
+ return !isContentHidden(props.portalMode, props.user);
9356
+ }
9342
9357
  function getFilterName(filter) {
9343
9358
  return filter?.attributeDescription?.name || "";
9344
9359
  }
@@ -9383,6 +9398,13 @@ function GridFilters(rawProps) {
9383
9398
  props.onFilterChange(filter, value);
9384
9399
  if (props.getSelectedFilters) props.getSelectedFilters();
9385
9400
  }
9401
+ function handleAvailabilityToggle(value, checked) {
9402
+ const next = checked ? [...availability, value] : availability.filter((v) => v !== value);
9403
+ setAvailability(next);
9404
+ setIsPending(true);
9405
+ if (props.onAvailabilityChange) props.onAvailabilityChange(next);
9406
+ if (props.getSelectedFilters) props.getSelectedFilters();
9407
+ }
9386
9408
  function handleMinChange(value) {
9387
9409
  const n = value > currentMax ? currentMax : value;
9388
9410
  setCurrentMin(n);
@@ -9456,11 +9478,16 @@ function GridFilters(rawProps) {
9456
9478
  setCurrentMin(props.priceMin || 0);
9457
9479
  setCurrentMax(props.priceMax || 9999);
9458
9480
  setExpandedFilters({});
9481
+ setAvailability([]);
9459
9482
  }, [props.clearSignal, props.priceMin, props.priceMax]);
9460
9483
  useEffect(() => {
9461
9484
  if (!props.activeTextFilters) return;
9462
9485
  setSelectedFilters(props.activeTextFilters);
9463
9486
  }, [props.activeTextFilters]);
9487
+ useEffect(() => {
9488
+ if (!props.activeAvailability) return;
9489
+ setAvailability([...props.activeAvailability]);
9490
+ }, [props.activeAvailability]);
9464
9491
  useEffect(() => {
9465
9492
  if (props.activePriceMin === void 0 && props.activePriceMax === void 0) {
9466
9493
  setCurrentMin(props.priceMin || 0);
@@ -9560,6 +9587,40 @@ function GridFilters(rawProps) {
9560
9587
  " ",
9561
9588
  /* @__PURE__ */ jsx("div", { className: "propeller-grid-filters__divider h-px bg-border-subtle" })
9562
9589
  ] }) : null,
9590
+ showAvailability() ? /* @__PURE__ */ jsxs(Fragment, { children: [
9591
+ /* @__PURE__ */ jsxs("div", { className: "propeller-grid-filters__availability space-y-3", children: [
9592
+ /* @__PURE__ */ jsx("h3", { className: "propeller-grid-filters__availability-title text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: getLabel(props.labels, "availability", "Availability") }),
9593
+ /* @__PURE__ */ jsxs("div", { className: "propeller-grid-filters__availability-options space-y-1.5", children: [
9594
+ /* @__PURE__ */ jsxs("label", { className: "propeller-grid-filters__option flex items-center gap-2 cursor-pointer group", children: [
9595
+ /* @__PURE__ */ jsx(
9596
+ "input",
9597
+ {
9598
+ type: "checkbox",
9599
+ className: "propeller-grid-filters__checkbox h-4 w-4 rounded border-input text-secondary focus:ring-secondary cursor-pointer flex-shrink-0",
9600
+ checked: availability.includes("in-stock"),
9601
+ disabled: !!props.isLoading,
9602
+ onChange: (e) => handleAvailabilityToggle("in-stock", e.target.checked)
9603
+ }
9604
+ ),
9605
+ /* @__PURE__ */ jsx("span", { className: "propeller-grid-filters__option-label flex-1 text-sm text-muted-foreground leading-none select-none group-hover:text-foreground", children: getLabel(props.labels, "inStock", "In stock") })
9606
+ ] }),
9607
+ /* @__PURE__ */ jsxs("label", { className: "propeller-grid-filters__option flex items-center gap-2 cursor-pointer group", children: [
9608
+ /* @__PURE__ */ jsx(
9609
+ "input",
9610
+ {
9611
+ type: "checkbox",
9612
+ className: "propeller-grid-filters__checkbox h-4 w-4 rounded border-input text-secondary focus:ring-secondary cursor-pointer flex-shrink-0",
9613
+ checked: availability.includes("out-of-stock"),
9614
+ disabled: !!props.isLoading,
9615
+ onChange: (e) => handleAvailabilityToggle("out-of-stock", e.target.checked)
9616
+ }
9617
+ ),
9618
+ /* @__PURE__ */ jsx("span", { className: "propeller-grid-filters__option-label flex-1 text-sm text-muted-foreground leading-none select-none group-hover:text-foreground", children: getLabel(props.labels, "outOfStock", "Out of stock") })
9619
+ ] })
9620
+ ] })
9621
+ ] }),
9622
+ /* @__PURE__ */ jsx("div", { className: "propeller-grid-filters__divider h-px bg-border-subtle" })
9623
+ ] }) : null,
9563
9624
  props.filters.length === 0 ? /* @__PURE__ */ jsx("p", { className: "propeller-grid-filters__empty text-sm text-foreground-subtle italic", children: getLabel(props.labels, "noFiltersAvailable", "No filters available") }) : null,
9564
9625
  getFilteredFilters()?.map((filter) => /* @__PURE__ */ jsxs("div", { className: "propeller-grid-filters__group border-b border-border-subtle pb-3 last:border-b-0", "data-expanded": isExpanded(getFilterName(filter)) ? "true" : "false", children: [
9565
9626
  /* @__PURE__ */ jsxs(
@@ -9735,6 +9796,8 @@ var DEFAULT_LABELS = {
9735
9796
  results: "results",
9736
9797
  perPage: " per page",
9737
9798
  price: "Price",
9799
+ inStock: "In stock",
9800
+ outOfStock: "Out of stock",
9738
9801
  switchToList: "Switch to list view",
9739
9802
  switchToGrid: "Switch to grid view"
9740
9803
  };
@@ -9763,7 +9826,8 @@ function GridToolbar(rawProps) {
9763
9826
  const text = props.activeTextFilters || {};
9764
9827
  const hasText = Object.keys(text).some((k) => (text[k] || []).length > 0);
9765
9828
  const hasPrice = props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0;
9766
- return hasText || hasPrice;
9829
+ const hasAvailability = (props.availability || []).length > 0;
9830
+ return hasText || hasPrice || hasAvailability;
9767
9831
  }
9768
9832
  function getActiveFilterBadges() {
9769
9833
  const text = props.activeTextFilters || {};
@@ -9781,6 +9845,9 @@ function GridToolbar(rawProps) {
9781
9845
  function isPriceSortDisabled() {
9782
9846
  return isContentHidden(props.portalMode, props.user);
9783
9847
  }
9848
+ function getAvailabilityLabel(value) {
9849
+ return value === "in-stock" ? getLabel56("inStock") : getLabel56("outOfStock");
9850
+ }
9784
9851
  function handleSortFieldChange(field) {
9785
9852
  setCurrentSortField(field);
9786
9853
  if (props.onSortChange) props.onSortChange(field, currentSortOrder);
@@ -9954,6 +10021,20 @@ function GridToolbar(rawProps) {
9954
10021
  ]
9955
10022
  }
9956
10023
  ) : null,
10024
+ (props.availability || []).map((value) => /* @__PURE__ */ jsxs(
10025
+ "span",
10026
+ {
10027
+ className: "propeller-grid-toolbar__filter-badge propeller-grid-toolbar__filter-badge--availability inline-flex items-center gap-1 cursor-pointer px-2.5 py-0.5 rounded-full text-xs font-semibold border border-input bg-background hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors",
10028
+ onClick: (event) => {
10029
+ if (props.onAvailabilityFilterRemove) props.onAvailabilityFilterRemove(value);
10030
+ },
10031
+ children: [
10032
+ getAvailabilityLabel(value),
10033
+ /* @__PURE__ */ jsx("span", { className: "propeller-grid-toolbar__filter-badge-remove", children: "\xD7" })
10034
+ ]
10035
+ },
10036
+ value
10037
+ )),
9957
10038
  getActiveFilterBadges()?.map((badge) => /* @__PURE__ */ jsxs(
9958
10039
  "span",
9959
10040
  {