@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/CHANGELOG.md CHANGED
@@ -8,6 +8,47 @@ once it reaches 1.0. Until then (the `0.x` line) the public API may change
8
8
  between minor versions; breaking changes are called out below and in
9
9
  [MIGRATION.md](./MIGRATION.md).
10
10
 
11
+ ## [0.13.0] - 2026-08-05
12
+
13
+ ### Added
14
+
15
+ - **Availability filter on `<GridFilters>`** — an opt-in section below the
16
+ price block with two checkboxes, In stock and Out of stock, wired to the
17
+ server-side stock filter the SDK exposes on `CategoryProductSearchInput`.
18
+ New props: `showAvailabilityFilter` (defaults `false`),
19
+ `activeAvailability` and `onAvailabilityChange`. Nothing selected sends no
20
+ filter, so the default listing is unchanged.
21
+
22
+ Selecting both buckets means "in stock or out of stock", which is every
23
+ product. The filter has no OR operator and the union is the unfiltered set
24
+ anyway, so both-selected sends no filter — the same as selecting neither.
25
+ The checkboxes stay ticked; only the request collapses.
26
+
27
+ The mapping lives in `propeller-v2-core-ui`'s `buildInventoryFilter`, so
28
+ this package never restates the operator semantics. Requires
29
+ `propeller-v2-core-ui` >= 0.5.0 and `propeller-sdk-v2` >= 0.15.0, where the
30
+ inventory filter was added.
31
+
32
+ - **`availability` and `onAvailabilityFilterRemove` on `<GridToolbar>`** —
33
+ the active-filter row now renders one removable chip per selected bucket,
34
+ so a stock-filtered listing shows why its results are narrowed and can be
35
+ undone from the results area. One chip per bucket, matching the attribute
36
+ chips rather than the price chip which covers a whole range. The row's
37
+ active check accounts for availability, so it appears when availability is
38
+ the only active filter.
39
+
40
+ - **`availability` on `<ProductGrid>`** — forwards the selection to
41
+ `useProductSearch`, which applies it to both the grid fetch and the
42
+ typeahead preview. Filtering only the grid would let the preview offer
43
+ products the grid then hides.
44
+
45
+ ### Changed
46
+
47
+ - The availability section is hidden for anonymous visitors in a semi-closed
48
+ portal, the same rule the price filter already follows. Hosts should pass
49
+ their own "show stock" setting through `showAvailabilityFilter`: filtering
50
+ by stock reads as broken when the cards display no stock to filter on.
51
+
11
52
  ## [0.12.0] - 2026-07-31
12
53
 
13
54
  ### Added
package/dist/index.cjs CHANGED
@@ -2261,7 +2261,8 @@ function useProductSearch(options) {
2261
2261
  userKey,
2262
2262
  JSON.stringify(options.textFilters ?? []),
2263
2263
  options.priceFilterMin ?? "",
2264
- options.priceFilterMax ?? ""
2264
+ options.priceFilterMax ?? "",
2265
+ JSON.stringify(options.availability ?? [])
2265
2266
  ].join("|");
2266
2267
  if (inflightFetches2.has(inflightKey)) {
2267
2268
  fetchIdRef.current--;
@@ -2303,6 +2304,7 @@ function useProductSearch(options) {
2303
2304
  }
2304
2305
  ] : [];
2305
2306
  const priceFilter = options.priceFilterMin !== void 0 || options.priceFilterMax !== void 0 ? { from: options.priceFilterMin ?? 0, to: options.priceFilterMax ?? 999999 } : void 0;
2307
+ const inventoryFilter = propellerV2CoreUi.buildInventoryFilter(options.availability);
2306
2308
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2307
2309
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2308
2310
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
@@ -2325,6 +2327,7 @@ function useProductSearch(options) {
2325
2327
  ...options.brand && { manufacturers: [options.brand] },
2326
2328
  ...options.textFilters?.length && { textFilters: options.textFilters },
2327
2329
  ...priceFilter && { price: priceFilter },
2330
+ ...inventoryFilter && { inventory: inventoryFilter },
2328
2331
  ...sortInputs.length && { sortInputs },
2329
2332
  ...options.companyId && { companyId: options.companyId },
2330
2333
  ...userId !== void 0 && { userId },
@@ -2401,6 +2404,7 @@ function useProductSearch(options) {
2401
2404
  options.textFilters,
2402
2405
  options.priceFilterMin,
2403
2406
  options.priceFilterMax,
2407
+ options.availability,
2404
2408
  options.sortField,
2405
2409
  options.sortOrder,
2406
2410
  options.companyId,
@@ -2440,6 +2444,7 @@ function useProductSearch(options) {
2440
2444
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2441
2445
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2442
2446
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
2447
+ const inventoryFilter = propellerV2CoreUi.buildInventoryFilter(options.availability);
2443
2448
  const categoryProductSearchInput = {
2444
2449
  language,
2445
2450
  page: 1,
@@ -2451,6 +2456,7 @@ function useProductSearch(options) {
2451
2456
  propellerSdkV2.ProductStatus.S
2452
2457
  ],
2453
2458
  hidden: false,
2459
+ ...inventoryFilter && { inventory: inventoryFilter },
2454
2460
  term,
2455
2461
  searchFields: [
2456
2462
  {
@@ -2530,6 +2536,7 @@ function useProductSearch(options) {
2530
2536
  JSON.stringify(options.textFilters),
2531
2537
  options.priceFilterMin,
2532
2538
  options.priceFilterMax,
2539
+ options.availability,
2533
2540
  options.sortField,
2534
2541
  options.sortOrder,
2535
2542
  pageSize,
@@ -9052,6 +9059,7 @@ function ProductGrid(rawProps) {
9052
9059
  textFilters: props.textFilters,
9053
9060
  priceFilterMin: props.priceFilterMin,
9054
9061
  priceFilterMax: props.priceFilterMax,
9062
+ availability: props.availability,
9055
9063
  sortField: props.sortField,
9056
9064
  sortOrder: props.sortOrder,
9057
9065
  pageSize: props.pageSize,
@@ -9354,11 +9362,18 @@ function GridFilters(rawProps) {
9354
9362
  const [isPending, setIsPending] = React6.useState(() => false);
9355
9363
  const [appliedMin, setAppliedMin] = React6.useState(() => void 0);
9356
9364
  const [appliedMax, setAppliedMax] = React6.useState(() => void 0);
9365
+ const [availability, setAvailability] = React6.useState(
9366
+ () => [...props.activeAvailability || []]
9367
+ );
9357
9368
  function showPriceFilter() {
9358
9369
  const mode = props.portalMode || "open";
9359
9370
  if (mode === "open") return true;
9360
9371
  return !!props.user;
9361
9372
  }
9373
+ function showAvailability() {
9374
+ if (!props.showAvailabilityFilter) return false;
9375
+ return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
9376
+ }
9362
9377
  function getFilterName(filter) {
9363
9378
  return filter?.attributeDescription?.name || "";
9364
9379
  }
@@ -9403,6 +9418,13 @@ function GridFilters(rawProps) {
9403
9418
  props.onFilterChange(filter, value);
9404
9419
  if (props.getSelectedFilters) props.getSelectedFilters();
9405
9420
  }
9421
+ function handleAvailabilityToggle(value, checked) {
9422
+ const next = checked ? [...availability, value] : availability.filter((v) => v !== value);
9423
+ setAvailability(next);
9424
+ setIsPending(true);
9425
+ if (props.onAvailabilityChange) props.onAvailabilityChange(next);
9426
+ if (props.getSelectedFilters) props.getSelectedFilters();
9427
+ }
9406
9428
  function handleMinChange(value) {
9407
9429
  const n = value > currentMax ? currentMax : value;
9408
9430
  setCurrentMin(n);
@@ -9476,11 +9498,16 @@ function GridFilters(rawProps) {
9476
9498
  setCurrentMin(props.priceMin || 0);
9477
9499
  setCurrentMax(props.priceMax || 9999);
9478
9500
  setExpandedFilters({});
9501
+ setAvailability([]);
9479
9502
  }, [props.clearSignal, props.priceMin, props.priceMax]);
9480
9503
  React6.useEffect(() => {
9481
9504
  if (!props.activeTextFilters) return;
9482
9505
  setSelectedFilters(props.activeTextFilters);
9483
9506
  }, [props.activeTextFilters]);
9507
+ React6.useEffect(() => {
9508
+ if (!props.activeAvailability) return;
9509
+ setAvailability([...props.activeAvailability]);
9510
+ }, [props.activeAvailability]);
9484
9511
  React6.useEffect(() => {
9485
9512
  if (props.activePriceMin === void 0 && props.activePriceMax === void 0) {
9486
9513
  setCurrentMin(props.priceMin || 0);
@@ -9580,6 +9607,40 @@ function GridFilters(rawProps) {
9580
9607
  " ",
9581
9608
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-grid-filters__divider h-px bg-border-subtle" })
9582
9609
  ] }) : null,
9610
+ showAvailability() ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9611
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability space-y-3", children: [
9612
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "propeller-grid-filters__availability-title text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: propellerV2CoreUi.getLabel(props.labels, "availability", "Availability") }),
9613
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability-options space-y-1.5", children: [
9614
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "propeller-grid-filters__option flex items-center gap-2 cursor-pointer group", children: [
9615
+ /* @__PURE__ */ jsxRuntime.jsx(
9616
+ "input",
9617
+ {
9618
+ type: "checkbox",
9619
+ className: "propeller-grid-filters__checkbox h-4 w-4 rounded border-input text-secondary focus:ring-secondary cursor-pointer flex-shrink-0",
9620
+ checked: availability.includes("in-stock"),
9621
+ disabled: !!props.isLoading,
9622
+ onChange: (e) => handleAvailabilityToggle("in-stock", e.target.checked)
9623
+ }
9624
+ ),
9625
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-grid-filters__option-label flex-1 text-sm text-muted-foreground leading-none select-none group-hover:text-foreground", children: propellerV2CoreUi.getLabel(props.labels, "inStock", "In stock") })
9626
+ ] }),
9627
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "propeller-grid-filters__option flex items-center gap-2 cursor-pointer group", children: [
9628
+ /* @__PURE__ */ jsxRuntime.jsx(
9629
+ "input",
9630
+ {
9631
+ type: "checkbox",
9632
+ className: "propeller-grid-filters__checkbox h-4 w-4 rounded border-input text-secondary focus:ring-secondary cursor-pointer flex-shrink-0",
9633
+ checked: availability.includes("out-of-stock"),
9634
+ disabled: !!props.isLoading,
9635
+ onChange: (e) => handleAvailabilityToggle("out-of-stock", e.target.checked)
9636
+ }
9637
+ ),
9638
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-grid-filters__option-label flex-1 text-sm text-muted-foreground leading-none select-none group-hover:text-foreground", children: propellerV2CoreUi.getLabel(props.labels, "outOfStock", "Out of stock") })
9639
+ ] })
9640
+ ] })
9641
+ ] }),
9642
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-grid-filters__divider h-px bg-border-subtle" })
9643
+ ] }) : null,
9583
9644
  props.filters.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "propeller-grid-filters__empty text-sm text-foreground-subtle italic", children: propellerV2CoreUi.getLabel(props.labels, "noFiltersAvailable", "No filters available") }) : null,
9584
9645
  getFilteredFilters()?.map((filter) => /* @__PURE__ */ jsxRuntime.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: [
9585
9646
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9755,6 +9816,8 @@ var DEFAULT_LABELS = {
9755
9816
  results: "results",
9756
9817
  perPage: " per page",
9757
9818
  price: "Price",
9819
+ inStock: "In stock",
9820
+ outOfStock: "Out of stock",
9758
9821
  switchToList: "Switch to list view",
9759
9822
  switchToGrid: "Switch to grid view"
9760
9823
  };
@@ -9783,7 +9846,8 @@ function GridToolbar(rawProps) {
9783
9846
  const text = props.activeTextFilters || {};
9784
9847
  const hasText = Object.keys(text).some((k) => (text[k] || []).length > 0);
9785
9848
  const hasPrice = props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0;
9786
- return hasText || hasPrice;
9849
+ const hasAvailability = (props.availability || []).length > 0;
9850
+ return hasText || hasPrice || hasAvailability;
9787
9851
  }
9788
9852
  function getActiveFilterBadges() {
9789
9853
  const text = props.activeTextFilters || {};
@@ -9801,6 +9865,9 @@ function GridToolbar(rawProps) {
9801
9865
  function isPriceSortDisabled() {
9802
9866
  return propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
9803
9867
  }
9868
+ function getAvailabilityLabel(value) {
9869
+ return value === "in-stock" ? getLabel56("inStock") : getLabel56("outOfStock");
9870
+ }
9804
9871
  function handleSortFieldChange(field) {
9805
9872
  setCurrentSortField(field);
9806
9873
  if (props.onSortChange) props.onSortChange(field, currentSortOrder);
@@ -9974,6 +10041,20 @@ function GridToolbar(rawProps) {
9974
10041
  ]
9975
10042
  }
9976
10043
  ) : null,
10044
+ (props.availability || []).map((value) => /* @__PURE__ */ jsxRuntime.jsxs(
10045
+ "span",
10046
+ {
10047
+ 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",
10048
+ onClick: (event) => {
10049
+ if (props.onAvailabilityFilterRemove) props.onAvailabilityFilterRemove(value);
10050
+ },
10051
+ children: [
10052
+ getAvailabilityLabel(value),
10053
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-grid-toolbar__filter-badge-remove", children: "\xD7" })
10054
+ ]
10055
+ },
10056
+ value
10057
+ )),
9977
10058
  getActiveFilterBadges()?.map((badge) => /* @__PURE__ */ jsxRuntime.jsxs(
9978
10059
  "span",
9979
10060
  {